Table of Contens

Linux shell 基本知识

目前市场上主要有三大主流操作系统,除了 windows 系列几乎不需要在终端工作,其它两 大操作系统大部份应该是和终端打交道。当然这和不同系统的定位不一样有很大的关系, W 系主要面向娱乐办公,Linux 系主要是用服务器系统,所以对于程序员来说,或多或少得面 对它。Mac 虽然面向个人用户,但是结合了前两者的优势,娱乐办公和开 发都可以完美应对。在使用 *nix(linux & Mac) 过程中,我们几乎每天都会对着终端一行 行的执行代码,解释我们的名令的程序就是我们所谓的 shell, 也是本文的所讲的对象。 shell 自从诞生以来,也出现各种版本,虽然实现不同,功能都是一样的。本文写个 shell 只是为了学习,没有其它目的。据本人所知,目前流行的 shell 有: bash , zsh , fish , xosh 等,这几种 shell 本人都用过,简单讲奖各自的特点吧, bash 不用说了吧,几乎 所有发行版的默认 shell。 zsh 集装逼与高效于一身的 shell。强烈建议于 oh-my-zsh 一 起使用,省去配置的时间。 fish 小众软件,智能补全,拥有类似 apt 一样的插件管理工 具,但是由于 shell 语法和 bash 不兼容,建议个人使用,不要用于工作中。 xosh 是用 python 实现的,所以终端直接执行 python 语句就可以执行,同时可以执行系统命令。 pythoner 可以尝试一波,随时随地测试 python 语句。

shell 工作流程

介绍完 shell , 下面简单讲讲工作流程:

首先,我们运行一个 shell 程序,它将阻塞等待用户的输入。当用户在标准输入上输入一 行命令文本,这时程序成功读入一行文本,不再阻塞,可以继续执行。接下来将读入的文本 进行切分,将其切分为命令和参数两部分。然后就可以 fork 一个子进程调用 exec 系统调 用运行切分好的命令,父进程 wait 等待子进程的执行结果。最后将执行结果反馈给用户。 等待下一行执行命名的出现。直到用户退出或者信号中断退出。

shell 实现

上面的工作流程说的比较简单,实现也是比较简单的。下面我们就一步步的来实现这个 shell:

本文使用 C++ 实现

可执行程序该有的样子

我们知道,每个程序都有一个入口函数,在 C++ 中,为 main 函数:

int main() {
  lsh::lshell lsh;
  lsh.loop();
  return 0;
}

本人的代码风格类似 linux 风格――小写加下滑线。其中的意思也是字面意思,先创建一 个 shell 实例,然后循环执行命令。

一探循环体究竟

在探索之前,我们先借助点小工具用来表示命令执行返回状态:

/* 命令返回状态 */
enum status_enum {
  fork_failed = -1,
  success = 0,
  exit_s = 1,  // 和后面的 exit 有冲突,因此在后边加上 _s
};

进入正菜:

inline void lshell::loop() {
  do {
    std::cout << ">>> ";
    read_line__();
    split_line__();
    excute__();
    clear__();
  } while (status_ == status_enum::success);
}

em~~~,和我们之前提到的工作流程一致吧。有些细心的读者发现我给的代码中多了 clear__(), 这个和本人的实现有关了,先留个疑问吧。继续探索。。。

解剖循环体

接下来就是将循环体解剖分开实现了。

读文本

首当其冲的是 read_line__:

inline void lshell::read_line__() { getline(std::cin, line_); }

em~~~~, 就一句话。虽然可以插入到其它代码块中,为了更好理解工作流程,我还是不省这 句代码了。虽然使用函数实现,不过不用担心性能,因为使用编译器关键字 inline 编译 的时候,编译器会用代码替换调用的部分,不会增加调用开销。

拆分文本

C++ 拆分字符串得自己实现,不过实现比较简单。如下:

void lshell::split_line__() {
  if (line_.empty()) return;
  std::string arg;
  int len = line_.length();
  int i = 0;
  while (i < len) {
    if (std::isblank(line_[i])) {
      args_.push_back(arg);
      arg.clear();
    } else {
      arg += line_[i];
    }
    ++i;
  }
  args_.push_back(arg);
}

我的代码写得比较 low , 当然还有其它方法实现,比如可以将 string 转换为 char*, 然 后配合 C 函数 strtok 将字符串分割。仁者见仁。

执行命令

shell 的内核所在,前面的都只是铺垫。

void lshell::excute__() {
  const int arg_len = args_.size();
  if (!arg_len) {
    status_setter(status_enum::success);
    return;
  }
  char **args = new char *[arg_len];
  for (int i = 0; i < arg_len; ++i) {
    args[i] = new char[args_[i].size() + 1];
    strncpy(args[i], args_[i].c_str(), args_[i].size());
  }
  auto is_builtin = builtins_.begin();
  if ((is_builtin = builtins_.find(args_[0])) != builtins_.end()) {
    status_setter(is_builtin->second(args));
  } else {
    status_setter(__launch(args));
  }
  for (auto i = 0; i < arg_len; ++i) delete args[i];
}

这里主要分为两部分,一个是内置命令,令一个是系统命令程序。

  • 内置命令
系统中没有实现如 cd, help, exit 等命令,所以需要我们手动实现这些命令,也即内置命
令。在本文代码中,我使用哈希表存储内置命令处理函数。

```C++
/* 内置命令:如 cd, help, exit... */
status_enum __cd(char **);
status_enum __help(char **);
inline status_enum __exit(char **) { return status_enum::exit_s; };
/* 内置命令对应的处理函数表,使用哈希表实现 */
const std::unordered_map<std::string, status_enum (*)(char **)> _builtins{
    {"cd", &__cd}, {"help", &__help}, {"exit", &__exit}};
```

后续添加内置命令也方便,不用修改 execute\_\_ 中的代码。以 cd 命令为例,看看内置命
令的实现,其实是调用系统调用:

```C++
status_enum __cd(char **args) {
  if (args[1] && int(args[1][0])) {
    if (chdir(args[1])) {
      std::cout << "The dir '" << args[1][0] << "' cannot be found! "
                << std::endl;
    }
  } else {
    std::cout << "Expected argument to \" cd \" " << std::endl;
  }
  return status_enum::success;
}
```
  • 系统命令
```C++
status_enum __launch(char **args) {
  pid_t pid = fork();
  if (pid > 0) {
    pid_t wpid;
    int status;
    do {
      wpid = waitpid(pid, &status, WUNTRACED);
    } while (!WIFEXITED(status) && !WIFSIGNALED(status));
    return status_enum::success;
  } else if (pid == 0) {
    if (execvp(args[0], args) == -1) {
      std::cout << "Connot found '" << args[0] << "' command. Please check!"
                << std::endl;
      exit(-1);
    }
  } else {
    std::cout << "Fork failed " << std::endl;
    return status_enum::fork_failed;
  }
  return status_enum::success;
}
```

这个就是一般的创建子进程,然后通过 execvp 系统调用执行环境变量里的程序,当然父进
程必须等待子进程执行完毕,将结果反馈给用户。

最后是清理

也就是清理上一次执行完的命令文本,本人的实现是将文本存储在私有变量里,避免了传参 过程产生的临时变量。虽然通过C++的一些高级语法避免这些开销,但是鄙人小菜一枚。所 以使用比较容易实现的方式,算是抛砖引玉吧,欢迎各位大佬指教。

总结

好了,到目前所有的核心代码几乎展示完毕了,这个是个小项目,主要是学习。 本人的完 整项目代码就不展示了,毕竟都是超级简单的东西。文末会给出项目的链接地址,作者是用 C写的,有兴趣可以参考他的实现。

参考

https://brennan.io/2015/01/16/write-a-shell-in-c/