用 c 语言编写 Shell

转自:Stephen Brennan's Blog 斯蒂芬 · 布伦南的博客 

Tutorial - Write a Shell in C • Stephen Brennan



Tutorial - Write a Shell in C

教程-用 c 语言编写 Shell

Stephen Brennan • 16 January 2015 2015年1月16日

It’s easy to view yourself as “not a real programmer.” There are programs out there that everyone uses, and it’s easy to put their developers on a pedestal. Although developing large software projects isn’t easy, many times the basic idea of that software is quite simple. Implementing it yourself is a fun way to show that you have what it takes to be a real programmer. So, this is a walkthrough on how I wrote my own simplistic Unix shell in C, in the hopes that it makes other people feel that way too.

人们很容易认为自己不是一个真正的程序员有一些程序是每个人都在使用的,很容易把他们的开发者放在一个基座上。尽管开发大型软件项目并不容易,但是很多时候这个软件的基本思想是相当简单的。自己实现它是一个有趣的方式来展示你有什么需要成为一个真正的程序员。因此,这是一个关于我如何用 c 语言编写我自己的简单的 Unix shell 的演练,希望它能让其他人也有同样的感觉。

The code for the shell described here, dubbed lsh, is available on GitHub.

这里所描述的 shell 代码名为 lsh,可以在 GitHub 上找到。

University students beware! Many classes have assignments that ask you to write a shell, and some faculty are aware of this tutorial and code. If you’re a student in such a class, you shouldn’t copy (or copy then modify) this code without permission. And even then, I would advise against heavily relying on this tutorial.

大学生们当心了!许多课程的作业要求你编写一个 shell,一些教师知道这个教程和代码。如果你是这门课的学生,你不应该未经允许复制(或复制然后修改)这些代码。即便如此,我还是建议不要过度依赖本教程。

Basic lifetime of a shell

壳的基本寿命

Let’s look at a shell from the top down. A shell does three main things in its lifetime.

让我们从上到下看一个 shell,shell 在其生命周期中主要做三件事。

  • Initialize 初始化: In this step, a typical shell would read and execute its configuration files. These change aspects of the shell’s behavior. : 在此步骤中,典型的 shell 将读取并执行其配置文件。这些改变了 shell 行为的各个方面
  • Interpret 翻译: Next, the shell reads commands from stdin (which could be interactive, or a file) and executes them. : 接下来,shell 从 stdin (可以是交互式的,也可以是文件)读取命令并执行它们
  • Terminate 终止: After its commands are executed, the shell executes any shutdown commands, frees up any memory, and terminates. : 在命令执行之后,shell 执行任何关闭命令,释放任何内存,并终止

These steps are so general that they could apply to many programs, but we’re going to use them for the basis for our shell. Our shell will be so simple that there won’t be any configuration files, and there won’t be any shutdown command. So, we’ll just call the looping function and then terminate. But in terms of architecture, it’s important to keep in mind that the lifetime of the program is more than just looping.

这些步骤非常通用,可以应用于许多程序,但是我们将把它们作为 shell 的基础。我们的 shell 将非常简单,不会有任何配置文件,也不会有任何 shutdown 命令。因此,我们只需调用循环函数,然后终止。但是在体系结构方面,重要的是要记住,程序的生命周期不仅仅是循环。

int main(int argc, char **argv)
{
  // Load config files, if any.

  // Run command loop.
  lsh_loop();

  // Perform any shutdown/cleanup.

  return EXIT_SUCCESS;
}

Here you can see that I just came up with a function, lsh_loop(), that will loop, interpreting commands. We’ll see the implementation of that next.

这里您可以看到,我刚刚创建了一个函数 lsh _ loop () ,它将循环解释命令。接下来我们将看到它的实现。

Basic loop of a shell

外壳的基本循环

So we’ve taken care of how the program should start up. Now, for the basic program logic: what does the shell do during its loop? Well, a simple way to handle commands is with three steps:

所以我们已经考虑了这个项目应该如何启动。现在,对于基本的程序逻辑: shell 在其循环期间做什么?一个简单的处理命令的方法有三个步骤:

  • Read 阅读: Read the command from standard input. : 从标准输入读取命令
  • Parse 解析: Separate the command string into a program and arguments. : 将命令字符串分离到程序和参数中
  • Execute 执行: Run the parsed command. : 运行解析命令

Here, I’ll translate those ideas into code for lsh_loop():

在这里,我将把这些想法转换成 lsh _ loop ()的代码:

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);
}

Let’s walk through the code. The first few lines are just declarations. The do-while loop is more convenient for checking the status variable, because it executes once before checking its value. Within the loop, we print a prompt, call a function to read a line, call a function to split the line into args, and execute the args. Finally, we free the line and arguments that we created earlier. Note that we’re using a status variable returned by lsh_execute() to determine when to exit.

让我们来学习一下代码。前几行只是声明。Do-while 循环更方便地检查状态变量,因为它在检查其值之前执行一次。在循环中,我们打印一个提示符,调用一个函数读取一行,调用一个函数将该行拆分为 args,然后执行 args。最后,我们释放前面创建的行和参数。注意,我们使用 lsh _ execute ()返回的状态变量来确定何时退出。

Reading a line

读一行

Reading a line from stdin sounds so simple, but in C it can be a hassle. The sad thing is that you don’t know ahead of time how much text a user will enter into their shell. You can’t simply allocate a block and hope they don’t exceed it. Instead, you need to start with a block, and if they do exceed it, reallocate with more space. This is a common strategy in C, and we’ll use it to implement lsh_read_line().

从 stdin 中读取一行听起来很简单,但在 c 语言中可能会很麻烦。可悲的是,你并不能提前知道用户将在 shell 中输入多少文本。你不能简单地分配一个块,然后希望它们不要超过它。相反,你需要从一个块开始,如果他们超过了,重新分配更多的空间。这是 c 语言中常用的策略,我们将使用它来实现 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) {
    // Read a character
    c = getchar();

    // If we hit EOF, replace it with a null character and return.
    if (c == EOF || c == '\n') {
      buffer[position] = '\0';
      return buffer;
    } else {
      buffer[position] = c;
    }
    position++;

    // If we have exceeded the buffer, reallocate.
    if (position >= bufsize) {
      bufsize += LSH_RL_BUFSIZE;
      buffer = realloc(buffer, bufsize);
      if (!buffer) {
        fprintf(stderr, "lsh: allocation error\n");
        exit(EXIT_FAILURE);
      }
    }
  }
}

The first part is a lot of declarations. If you hadn’t noticed, I prefer to keep the old C style of declaring variables before the rest of the code. The meat of the function is within the (apparently infinite) while (1) loop. In the loop, we read a character (and store it as an int, not a char, that’s important! EOF is an integer, not a character, and if you want to check for it, you need to use an int. This is a common beginner C mistake.). If it’s the newline, or EOF, we null terminate our current string and return it. Otherwise, we add the character to our existing string.

第一部分是大量的声明。如果您没有注意到的话,我倾向于在代码的其余部分之前保留声明变量的老式 c 风格。函数的主要部分在 while (1)循环中(显然是无限的)。在循环中,我们读取一个字符(并将其存储为 int,而不是 char,这很重要!EOF 是整数,不是字符,如果要检查它,需要使用 int。这是初学者常犯的错误。).如果它是换行符,或者 EOF,我们使用 null 结束当前的字符串并返回它。否则,我们将字符添加到现有的字符串中。

Next, we see whether the next character will go outside of our current buffer size. If so, we reallocate our buffer (checking for allocation errors) before continuing. And that’s really it.

接下来,我们看看下一个字符是否会超出我们当前的缓冲区大小。如果是这样,我们在继续之前重新分配我们的缓冲区(检查分配错误)。就是这样。

Those who are intimately familiar with newer versions of the C library may note that there is a getline() function in stdio.h that does most of the work we just implemented. To be completely honest, I didn’t know it existed until after I wrote this code. This function was a GNU extension to the C library until 2008, when it was added to the specification, so most modern Unixes should have it now. I’m leaving my existing code the way it is, and I encourage people to learn it this way first before using getline. You’d be robbing yourself of a learning opportunity if you didn’t! Anyhow, with getline, the function becomes easier:

对 c 库的新版本非常熟悉的人可能会注意到 stdio.h 中有一个 getline ()函数,它完成了我们刚刚实现的大部分工作。老实说,直到我写了这段代码,我才知道它的存在。这个函数是 c 库的 GNU 扩展,直到2008年才被添加到规范中,所以大多数现代 unix 系统现在都应该有这个函数。我保持现有代码的原样,并且我鼓励人们在使用 getline 之前先以这种方式学习它。如果你不这样做,你就是在剥夺自己的学习机会!不管怎样,使用 getline,函数变得更简单:

char *lsh_read_line(void)
{
  char *line = NULL;
  ssize_t bufsize = 0; // have getline allocate a buffer for us

  if (getline(&line, &bufsize, stdin) == -1){
    if (feof(stdin)) {
      exit(EXIT_SUCCESS);  // We recieved an EOF
    } else  {
      perror("readline");
      exit(EXIT_FAILURE);
    }
  }

  return line;
}

This is not 100% trivial because we still need to check for EOF or errors while reading. EOF (end of file) means that either we were reading commands from a text file which we’ve reached the end of, or the user typed Ctrl-D, which signals end-of-file. Either way, it means we should exit successfully, and if any other error occurs, we should fail after printing the error.

这不是100% 的琐碎,因为我们仍然需要检查 EOF 或读取时的错误。EOF (文件结束)意味着我们要么从文本文件中读取命令,要么用户输入 Ctrl-D,表示文件结束。无论哪种方式,它都意味着我们应该成功退出,如果发生任何其他错误,我们应该在打印错误后失败。

Parsing the line

解析这一行

OK, so if we look back at the loop, we see that we now have implemented lsh_read_line(), and we have the line of input. Now, we need to parse that line into a list of arguments. I’m going to make a glaring simplification here, and say that we won’t allow quoting or backslash escaping in our command line arguments. Instead, we will simply use whitespace to separate arguments from each other. So the command echo "this message" would not call echo with a single argument this message, but rather it would call echo with two arguments: "this and message".

好的,如果我们回头看一下这个循环,我们看到我们现在已经实现了 lsh _ read _ line () ,并且我们有了输入行。现在,我们需要将该行解析为一个参数列表。这里我要做一个明显的简化,我们不允许在命令行参数中引用或反斜杠转义。相反,我们将简单地使用空格将参数彼此分隔开。因此,命令 echo“ this message”不会用单个参数调用 echo,而是用两个参数调用 echo: “ this and message”。

With those simplifications, all we need to do is “tokenize” the string using whitespace as delimiters. That means we can break out the classic library function strtok to do some of the dirty work for us.

通过这些简化,我们需要做的就是使用空格作为分隔符“标记”字符串。这意味着我们可以突破传统的库函数 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;
}

If this code looks suspiciously similar to lsh_read_line(), it’s because it is! We are using the same strategy of having a buffer and dynamically expanding it. But this time, we’re doing it with a null-terminated array of pointers instead of a null-terminated array of characters.

如果这段代码看起来与 lsh _ read _ line ()非常相似,那是因为它确实如此!我们正在使用相同的策略,即拥有一个缓冲区并动态地扩展它。但是这一次,我们使用的是一个以 null 结尾的指针数组,而不是以 null 结尾的字符数组。

At the start of the function, we begin tokenizing by calling strtok. It returns a pointer to the first token. What strtok() actually does is return pointers to within the string you give it, and place \0 bytes at the end of each token. We store each pointer in an array (buffer) of character pointers.

在函数开始时,我们通过调用 strtok 开始标记。它返回一个指向第一个令牌的指针。Strtok ()实际上做的是返回指向字符串的指针,并在每个标记的末尾放置0个字节。我们将每个指针存储在字符指针的数组(缓冲区)中。

Finally, we reallocate the array of pointers if necessary. The process repeats until no token is returned by strtok, at which point we null-terminate the list of tokens.

最后,如果需要,我们重新分配指针数组。重复这个过程,直到 strtok 没有返回任何令牌,这时我们使用 null 结束令牌列表。

So, once all is said and done, we have an array of tokens, ready to execute. Which begs the question, how do we do that?

因此,一旦完成了所有的操作,我们就有了一个标记数组,可以执行了。这就引出了一个问题,我们该怎么做呢?

How shells start processes

Shell 是如何启动进程的

Now, we’re really at the heart of what a shell does. Starting processes is the main function of shells. So writing a shell means that you need to know exactly what’s going on with processes and how they start. That’s why I’m going to take us on a short diversion to discuss processes in Unix-like operating systems.

现在,我们正处于 shell 的核心。启动过程是外壳的主要功能。因此,编写 shell 意味着您需要准确地知道进程发生了什么以及它们是如何启动的。这就是为什么我要简短地转移我们的注意力,来讨论类 unix 操作系统中的进程。

There are only two ways of starting processes on Unix. The first one (which almost doesn’t count) is by being Init. You see, when a Unix computer boots, its kernel is loaded. Once it is loaded and initialized, the kernel starts only one process, which is called Init. This process runs for the entire length of time that the computer is on, and it manages loading up the rest of the processes that you need for your computer to be useful.

在 Unix 上启动进程只有两种方式。第一个(几乎不算)是 Init。你看,当一台 Unix 计算机引导时,它的内核就被加载了。加载和初始化后,内核只启动一个进程,称为 Init。这个进程在计算机打开的整个时间段内运行,并且它管理加载计算机有用所需的其余进程。

Since most programs aren’t Init, that leaves only one practical way for processes to get started: the fork() system call. When this function is called, the operating system makes a duplicate of the process and starts them both running. The original process is called the “parent”, and the new one is called the “child”. fork() returns 0 to the child process, and it returns to the parent the process ID number (PID) of its child. In essence, this means that the only way for new processes is to start is by an existing one duplicating itself.

由于大多数程序都不是 Init,因此只有一种实用的方法可以启动进程: fork ()系统调用。当调用这个函数时,操作系统会复制进程并启动它们。最初的过程被称为“父”,而新的过程被称为“子”。Fork ()将0返回给子进程,并将其子进程的进程 ID 号(PID)返回给父进程。从本质上讲,这意味着新进程的唯一方式是启动一个自我复制的现有进程。

This might sound like a problem. Typically, when you want to run a new process, you don’t just want another copy of the same program – you want to run a different program. That’s what the exec() system call is all about. It replaces the current running program with an entirely new one. This means that when you call exec, the operating system stops your process, loads up the new program, and starts that one in its place. A process never returns from an exec() call (unless there’s an error).

这听起来可能是个问题。通常,当您想要运行一个新进程时,您不只是想要同一个程序的另一个副本——您想要运行一个不同的程序。这就是 exec ()系统调用的全部内容。它用一个全新的程序替换当前的运行程序。这意味着,当您调用 exec 时,操作系统将停止您的进程,加载新程序,并在其位置启动该程序。进程从不从 exec ()调用返回(除非有错误)。

With these two system calls, we have the building blocks for how most programs are run on Unix. First, an existing process forks itself into two separate ones. Then, the child uses exec() to replace itself with a new program. The parent process can continue doing other things, and it can even keep tabs on its children, using the system call wait().

有了这两个系统调用,我们就有了在 Unix 上如何运行大多数程序的构建模块。首先,一个现有的进程将自己分割成两个独立的进程。然后,子程序使用 exec ()将自己替换为一个新程序。父进程可以继续做其他事情,甚至可以使用系统调用 wait ()来监视其子进程。

Phew! That’s a lot of information, but with all that background, the following code for launching a program will actually make sense:

呼!这里有很多信息,但是有了这些背景知识,下面这些启动程序的代码实际上是有意义的:

int lsh_launch(char **args)
{
  pid_t pid, wpid;
  int status;

  pid = fork();
  if (pid == 0) {
    // Child process
    if (execvp(args[0], args) == -1) {
      perror("lsh");
    }
    exit(EXIT_FAILURE);
  } else if (pid < 0) {
    // Error forking
    perror("lsh");
  } else {
    // Parent process
    do {
      wpid = waitpid(pid, &status, WUNTRACED);
    } while (!WIFEXITED(status) && !WIFSIGNALED(status));
  }

  return 1;
}

Alright. This function takes the list of arguments that we created earlier. Then, it forks the process, and saves the return value. Once fork() returns, we actually have two processes running concurrently. The child process will take the first if condition (where pid == 0).

好吧。这个函数接受我们前面创建的参数列表。然后,它对进程进行分支,并保存返回值。一旦 fork ()返回,实际上有两个进程并发运行。子进程将采用第一个 if 条件(其中 pid = 0)。

In the child process, we want to run the command given by the user. So, we use one of the many variants of the exec system call, execvp. The different variants of exec do slightly different things. Some take a variable number of string arguments. Others take a list of strings. Still others let you specify the environment that the process runs with. This particular variant expects a program name and an array (also called a vector, hence the ‘v’) of string arguments (the first one has to be the program name). The ‘p’ means that instead of providing the full file path of the program to run, we’re going to give its name, and let the operating system search for the program in the path.

在子进程中,我们希望运行用户给出的命令。因此,我们使用 exec 系统调用的众多变体之一 execvp。不同的 exec 变体做的事情略有不同。有些接受数量可变的字符串参数。另一些则采用字符串列表。还有一些方法允许您指定流程运行的环境。这个特殊的变体需要一个程序名和一个字符串参数的数组(也称为向量,因此是‘ v’)(第一个必须是程序名)。“ p”意味着不提供要运行的程序的完整文件路径,而是给出它的名称,让操作系统搜索路径中的程序。

If the exec command returns -1 (or actually, if it returns at all), we know there was an error. So, we use perror to print the system’s error message, along with our program name, so users know where the error came from. Then, we exit so that the shell can keep running.

如果 exec 命令返回 -1(或者实际上,如果它返回的话) ,我们就知道有错误。因此,我们使用 perror 打印系统的错误消息以及程序名,以便用户知道错误来自哪里。然后,我们退出,以便 shell 可以继续运行。

The second condition (pid < 0) checks whether fork() had an error. If so, we print it and keep going – there’s no handling that error beyond telling the user and letting them decide if they need to quit.

第二个条件(pid < 0)检查 fork ()是否有错误。如果是这样,我们打印它并继续-没有处理这个错误,除了告诉用户并让他们决定是否需要退出。

The third condition means that fork() executed successfully. The parent process will land here. We know that the child is going to execute the process, so the parent needs to wait for the command to finish running. We use waitpid() to wait for the process’s state to change. Unfortunately, waitpid() has a lot of options (like exec()). Processes can change state in lots of ways, and not all of them mean that the process has ended. A process can either exit (normally, or with an error code), or it can be killed by a signal. So, we use the macros provided with waitpid() to wait until either the processes are exited or killed. Then, the function finally returns a 1, as a signal to the calling function that we should prompt for input again.

第三个条件意味着 fork ()成功执行。父进程将在这里着陆。我们知道子进程将要执行进程,因此父进程需要等待命令完成运行。我们使用 waitpid ()等待进程的状态更改。不幸的是,waitpid ()有很多选项(比如 exec ())。流程可以通过许多方式改变状态,并不是所有的流程都意味着流程已经结束。进程可以退出(正常情况下,或带有错误代码) ,也可以被信号杀死。因此,我们使用 waitpid ()提供的宏来等待进程退出或终止。然后,该函数最终返回1,作为调用函数的信号,我们应该再次提示输入。

Shell Builtins

壳牌内置产品

You may have noticed that the lsh_loop() function calls lsh_execute(), but above, we titled our function lsh_launch(). This was intentional! You see, most commands a shell executes are programs, but not all of them. Some of them are built right into the shell.

您可能已经注意到 lsh _ loop ()函数调用 lsh _ execute () ,但是在上面,我们将函数命名为 lsh _ launch ()。这是故意的!你看,shell 执行的大多数命令都是程序,但并不是所有的程序。它们中的一些就建在外壳里。

The reason is actually pretty simple. If you want to change directory, you need to use the function chdir(). The thing is, the current directory is a property of a process. So, if you wrote a program called cd that changed directory, it would just change its own current directory, and then terminate. Its parent process’s current directory would be unchanged. Instead, the shell process itself needs to execute chdir(), so that its own current directory is updated. Then, when it launches child processes, they will inherit that directory too.

原因其实很简单。如果要更改目录,则需要使用函数 chdir ()。问题是,工作目录是一个过程的属性。所以,如果你写了一个叫做 cd 的程序改变了目录,它只会改变自己的工作目录,然后终止。其母公司的工作目录将保持不变。相反,shell 进程本身需要执行 chdir () ,以便更新它自己的工作目录。然后,当启动子进程时,它们也将继承该目录。

Similarly, if there was a program named exit, it wouldn’t be able to exit the shell that called it. That command also needs to be built into the shell. Also, most shells are configured by running configuration scripts, like ~/.bashrc. Those scripts use commands that change the operation of the shell. These commands could only change the shell’s operation if they were implemented within the shell process itself.

类似地,如果有一个名为 exit 的程序,它就不能退出调用它的 shell。该命令还需要内置到 shell 中。另外,大多数 shell 都是通过运行配置脚本来配置的,比如 ~/。巴什克。这些脚本使用改变 shell 操作的命令。如果这些命令是在 shell 进程本身中实现的,那么它们只能更改 shell 的操作。

So, it makes sense that we need to add some commands to the shell itself. The ones I added to my shell are cdexit, and help. Here are their function implementations below:

因此,我们需要向 shell 本身添加一些命令是有意义的。我添加到 shell 中的是 cd、 exit 和 help。下面是它们的函数实现:

/*
  Function Declarations for builtin shell commands:
 */
int lsh_cd(char **args);
int lsh_help(char **args);
int lsh_exit(char **args);

/*
  List of builtin commands, followed by their corresponding functions.
 */
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 *);
}

/*
  Builtin function implementations.
*/
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("Stephen Brennan's 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;
}

There are three parts to this code. The first part contains forward declarations of my functions. A forward declaration is when you declare (but don’t define) something, so that you can use its name before you define it. The reason I do this is because lsh_help() uses the array of builtins, and the arrays contain lsh_help(). The cleanest way to break this dependency cycle is by forward declaration.

这个代码有三个部分。第一部分包含函数的前向声明。前向声明是指声明(但不定义)某个东西,以便在定义它之前可以使用它的名称。我这样做的原因是 lsh _ help ()使用内置数组,而数组包含 lsh _ help ()。打破这种依赖循环的最干净的方法是前向声明。

The next part is an array of builtin command names, followed by an array of their corresponding functions. This is so that, in the future, builtin commands can be added simply by modifying these arrays, rather than editing a large “switch” statement somewhere in the code. If you’re confused by the declaration of builtin_func, that’s OK! I am too. It’s an array of function pointers (that take array of strings and return an int). Any declaration involving function pointers in C can get really complicated. I still look up how function pointers are declared myself!1

下一部分是一个内建命令名数组,后面是它们对应的函数数组。因此,将来可以通过修改这些数组来添加内置命令,而不是在代码中编辑大的“ switch”语句。如果你对 builtin _ func 的声明感到困惑,那没关系!我也是。它是一个函数指针数组(接受字符串数组并返回一个 int)。在 c 语言中任何涉及函数指针的声明都会变得非常复杂。我仍然查看函数指针是如何声明的!1

Finally, I implement each function. The lsh_cd() function first checks that its second argument exists, and prints an error message if it doesn’t. Then, it calls chdir(), checks for errors, and returns. The help function prints a nice message and the names of all the builtins. And the exit function returns 0, as a signal for the command loop to terminate.

最后,我实现了每个函数。Lsh _ cd ()函数首先检查其第二个参数是否存在,如果不存在则输出错误消息。然后,它调用 chdir () ,检查错误并返回。Help 函数打印一个漂亮的消息和所有内置函数的名称。退出函数返回0,作为命令循环终止的信号。

Putting together builtins and processes

组装内置程序和过程

The last missing piece of the puzzle is to implement lsh_execute(), the function that will either launch a builtin, or a process. If you’re reading this far, you’ll know that we’ve set ourselves up for a really simple function:

最后一个遗漏的部分是实现 lsh _ execute () ,这个函数将启动一个内建函数或一个进程。如果你读到这里,你就会知道我们为自己设置了一个非常简单的函数:

int lsh_execute(char **args)
{
  int i;

  if (args[0] == NULL) {
    // An empty command was entered.
    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);
}

All this does is check if the command equals each builtin, and if so, run it. If it doesn’t match a builtin, it calls lsh_launch() to launch the process. The one caveat is that args might just contain NULL, if the user entered an empty string, or just whitespace. So, we need to check for that case at the beginning.

所做的就是检查命令是否等于每个内建,如果等于,就运行它。如果它不匹配内置语句,则调用 lsh _ launch ()来启动进程。一个需要注意的问题是,如果用户输入空字符串或者只输入空白,args 可能只包含 NULL。所以,我们需要在一开始就检查一下这个案子。

Putting it all together

把它们放在一起

That’s all the code that goes into the shell. If you’ve read along, you should understand completely how the shell works. To try it out (on a Linux machine), you would need to copy these code segments into a file (main.c), and compile it. Make sure to only include one implementation of lsh_read_line(). You’ll need to include the following headers at the top. I’ve added notes so that you know where each function comes from.

这是进入 shell 的所有代码。如果您已经阅读了这篇文章,那么您应该完全理解 shell 是如何工作的。要在一台 Linux 机器上进行尝试,您需要将这些代码段复制到一个文件(main.c)中,并编译它。确保只包含 lsh _ read _ line ()的一个实现。您需要在顶部包含以下标题。我添加了注释,这样您就可以知道每个函数的来源。

  • #include <sys/wait.h>
    • waitpid() and associated macros 以及相关的宏
  • #include <unistd.h>
    • chdir()
    • fork()
    • exec()
    • pid_t
  • #include <stdlib.h>
    • malloc()
    • realloc()
    • free()
    • exit()
    • execvp()
    • EXIT_SUCCESSEXIT_FAILURE
  • #include <stdio.h>
    • fprintf()
    • printf()
    • stderr
    • getchar()
    • perror()
  • #include <string.h>
    • strcmp()
    • strtok()

Once you have the code and headers, it should be as simple as running gcc -o main main.c to compile it, and then ./main to run it.

一旦有了代码和头文件,就应该像运行 gcc-o main mainmain.c 编译它一样简单,然后。让我来运行它。

Alternatively, you can get the code from GitHub. That link goes straight to the current revision of the code at the time of this writing– I may choose to update it and add new features someday in the future. If I do, I’ll try my best to update this article with the details and implementation ideas.

或者,你可以从 GitHub 上获取代码。在撰写本文时,该链接直接指向当前的代码修订版本——我可能会选择更新它,并在将来的某一天添加新的特性。如果我这样做了,我将尽最大努力用详细信息和实现思想来更新本文。

Wrap up

收工

If you read this and wondered how in the world I knew how to use those system calls, the answer is simple: man pages. In man 3p there is thorough documentation on every system call. If you know what you’re looking for, and you just want to know how to use it, the man pages are your best friend. If you don’t know what sort of interface the C library and Unix offer you, I would point you toward the POSIX Specification, specifically Section 13, “Headers”. You can find each header and everything it is required to define in there.

如果你读了这篇文章,想知道我是怎么知道如何使用这些系统调用的,答案很简单: 手册页。在 man 3p 中,每个系统调用都有详尽的文档说明。如果你知道你在寻找什么,你只是想知道如何使用它,手册页是你最好的朋友。如果您不知道 c 库和 Unix 提供了什么样的接口,我会向您介绍 POSIX 规范,特别是第13节“ header”。你可以在那里找到每个头和所有需要定义的东西。

Obviously, this shell isn’t feature-rich. Some of its more glaring omissions are:

显然,这个 shell 并不是功能丰富的。它有一些更明显的遗漏:

  • Only whitespace separating arguments, no quoting or backslash escaping. 只有空格分隔参数,没有引号或反斜杠转义
  • No piping or redirection. 没有管道或重定向
  • Few standard builtins. 几乎没有标准的内建物
  • No globbing. 没有球状物

The implementation of all of this stuff is really interesting, but way more than I could ever fit into an article like this. If I ever get around to implementing any of them, I’ll be sure to write a follow-up about it. But I’d encourage any reader to try implementing this stuff yourself. If you’re met with success, drop me a line in the comments below, I’d love to see the code.

所有这些东西的实现真的很有趣,但是远远超出了我能够放进这样一篇文章中的能力。如果我有机会实施其中的任何一个,我一定会写一篇关于它的后续文章。但我鼓励任何读者尝试自己实现这些东西。如果你获得了成功,请在下面的评论栏写下你的想法,我很想看看你的代码。

And finally, thanks for reading this tutorial (if anyone did). I enjoyed writing it, and I hope you enjoyed reading it. Let me know what you think in the comments!

最后,感谢阅读本教程(如果有人读过的话)。我喜欢写这本书,我希望你也喜欢读它。请在评论中告诉我你的想法!

Edit: In an earlier version of this article, I had a couple nasty bugs in lsh_split_line(), that just happened to cancel each other out. Thanks to /u/munmap on Reddit (and other commenters) for catching them! Check this diff to see exactly what I did wrong.

编辑: 在本文的早期版本中,lsh _ split _ line ()中有两个讨厌的 bug,它们刚好相互抵消。感谢 Reddit (和其他评论者)上的/u/munmap 抓住了他们!检查这个差异,看看我到底做错了什么。

Edit 2: Thanks to user ghswa on GitHub for contributing some null checks for malloc() that I forgot. He/she also pointed out that the manpage for getline() specifies that the first argument should be freeable, so line should be initialized to NULL in my lsh_read_line() implementation that uses getline().

编辑2: 感谢 GitHub 上的用户 ghswa 为我忘记的 malloc ()提供了一些 null 检查。他/她还指出 getline ()的 manpage 指定第一个参数应该是可释放的,因此在使用 getline ()的 lsh _ read _ line ()实现中,第一行应该初始化为 NULL。

Edit 3: It’s 2020 and we’re still finding bugs, this is why software is hard. Credit to harishankarv on Github, for finding an issue with my “simple” implementation of lsh_read_line() that depends on getline(). See this issue for details – the text of the blog is updated.

编辑3: 现在是2020年,我们仍然在寻找漏洞,这就是为什么软件是困难的。感谢 Github 上的 harishankarv,因为它发现了 lsh _ read _ line ()的“简单”实现中的一个问题,该实现依赖于 getline ()。详情请参阅此问题——博客文本已更新。

Footnotes

脚注

  1. Edit 4/Footnote: It’s 2021, over 6.5 years since writing this tutorial. I now work on operating systems in C for a living. I just wanted to say that I still do not remember how to declare a function pointer. I still need to Google it every time. 

    编辑4/脚注: 现在是2021年,写这篇教程已经6.5年了。我现在以研究 c 语言的操作系统为生。我只是想说,我仍然不记得如何申报函数指针。我每次都需要谷歌一下。

posted @ 2021-09-26 23:20  泥烟  阅读(106)  评论(0编辑  收藏  举报