[译] 用 Go 语言写一个简单的 shell

JuddArthur 发布于1年前
0 条问题

本文我们将用Go语言实现一个最简的Unix shell。这段程序只有60行左右。阅读之前你需要对Go语言有一些了解(例如,如何构建一个简单的项目),并且知道一些UNIX shell的基础用法。

开始之前

" UNIX非常简单,只有天才才明白它的简单 "

——Dennis Ritchie ( https://en.wikipedia.org/wiki/Dennis_Ritchie )

当然,我不是一个天才,并且我甚至都不确定 Dennis Ritchie的话是否 包括用户空间工具。此外,对于一个全功能的操作系统而言, shell 只是其中一块很小的部分(与 Kernel 对比来说, shell 真的是一个简单的部分),但是我希望在文章的最后,你可以惊讶的发现,一旦你理解了 shell 背后的概念,实现一个 shell 是多么 的 简单。

什么是Shell

Shell通常很难定义,我将 shell 定义为操作系统的一个基本的用户接口,你可以在 shell 中输入命令并接收对应的输出。当需要更多的信息或更好的定义时,你可以查看维基百科的文章( https://en.wikipedia.org/wiki/Shell_(computing) )

关于shell的一些例子有:

  • Bash( https://en.wikipedia.org/wiki/Bash_(Unix_shell) )

  • Zsh( https://en.wikipedia.org/wiki/Z_shell )

  • Gnome Shell( https://en.wikipedia.org/wiki/GNOME_Shell )

  • Windows Shell( https://en.wikipedia.org/wiki/Windows_shell )

Gnome和Windows的图形用户接口是shell类型的接口,但大多数IT相关人士(最起码是我)当谈及shell时宁愿是基于文本的。例如,列表中的前两个,将描述一个简单切无图形化的shell。

事实上,这个功能是解释给定一个输入命令,以及接收对应的输出。例如,运行程序ls将会展示当前目录下的内容。

输入:

ls

输出:

Applications etc

Library home

...

这就是shell,超级简单,让我们开始吧!

输入循环

执行一个命令,我们将接收输入,我们使用键盘完成这些输入。

键盘是我们的标准输入装置(os.Stdin),我们可以创建一个阅读器去访问键盘。每次我们按下回车键,就会创建一个新行。新行通过\n进行标记。当按下回车键是,任何的写入都会被存储到输入变量中。

reader := bufio.NewReader(os.Stdin)

input, err := reader.ReadString(‘\n')

让我们在main.go 文件中放入一个主函数,并围绕ReadString 功能添加一个for循环,我们可以连续的输入命令。在读取输入过程中发生错误时,我们会通过标准错误设备打印该错误(os.Stderr)。如果我们使用fmt.Println而不使用特殊的输出设备,错误信息将会被指向标准输出设备(os.Stdout)。shell本身不能改变这个功能性,但单独的设备允许对输出进行简单的过滤,以便做进一步处理。

func main() {

reader := bufio.NewReader(os.Stdin)

for {

// Read the keyboad input.

input, err := reader.ReadString('\n')

if err != nil {

fmt.Fprintln(os.Stderr, err)

}

}

}

执行命令

现在,我们想执行输入的命令。让我们为其创建一个新的函数 execInput , execInput 取一个字符串作为变量。首先,我们在输入的最后删除新行控制字符 \n 。其次,我们为 exec.Command ( input )准备一个命令,并为这个命令设置对应的输出和错误装置。最后,我们准备 cmd.Run( ) 过程命令。

func execInput(input string) error {

// Remove the newline character.

input = strings.TrimSuffix(input, "\n")

// Prepare the command to execute.

cmd := exec.Command(input)

// Set the correct output device.

cmd.Stderr = os.Stderr

cmd.Stdout = os.Stdout

// Execute the command and save it's output.

err := cmd.Run()

if err != nil {

     return err

}

return nil

}

第一个原型

通过在循环的顶端添加一个输入指示器( > ),以及在循环的底端添加一个新的 execInput 函数完成了主函数。

func main() {

reader := bufio.NewReader(os.Stdin)

for {

fmt.Print("> ")

// Read the keyboad input.

input, err := reader.ReadString('\n')

if err != nil {

fmt.Println(err)

}

// Handle the execution of the input.

err = execInput(input)

if err != nil {

fmt.Println(err)

}

}

}

该做第一个测试了。Go语言通过运行main.go来构建和运行shell。当你看到输入指示>时,就可以写一些命令了。例如,我们可以运行ls命令。

> ls

LICENSE

main.go

main_test.go

Wow,成功了!ls被执行了,且给我们展示了当前目录下的内容。退出shell同其他程序一样,通过CTRL-C组合键即可。

通过 ls -l 命令得到更长形式的列表。

> ls -l

exec: "ls -l": executable file not found in $PATH

这种格式不再起作用了,这是因为我们的shell试着运行无法找到的ls -l的程序,ls和-l既是一段程序,-l 也同样被称作变量,被程序自身解析。目前,我们不能区分命令和变量。为了修复这个缺点,我们必须修改execLine函数,将输入使用空格进行分离。

func execInput(input string) error {

// Remove the newline character.

input = strings.TrimSuffix(input, “\n")

// Split the input to separate the command and the arguments.

args := strings.Split(input, " “)

// Pass the program and the arguments separately.

cmd := exec.Command(args[0], args[1:]...)

...

}

这段程序的名称现在被存到args[0]中,变量在随后的索引中。现在运行ls -l就会得到我们想要的结果。

> ls -l

total 24

-rw-r--r-- 1 simon staff 1076 30 Jun 09:49 LICENSE

-rw-r--r-- 1 simon staff 1058 30 Jun 10:10 main.go

-rw-r--r-- 1 simon staff 897 30 Jun 09:49 main_test.go

改变目录(cd)

现在我们可以通过一系列独立的变量运行命令。对最小可用集合设置一些功能点是很有必要的,现在只剩下一件事了(最起码根据我的看法)。当你演示 shell 时你可能已经偶遇了这些:你使用 cd 命令并不能改变目录。

> cd /

> ls

LICENSE

main.go

main_test.go

很明显这不是我根目录下的内容。为什么cd命令没有起作用?如果你知道真实路径,那就非常简单了( https:/ /stackoveryow.com/a/38776411) cd程序是shell一个内建的命令。

再一次,我们需要修改execInput函数。在Split函数之后,我们需要把存储在args[0]的第一个变量(执行的命令)进行一个状态转换。当命令为cd时,我们检查后面是否有变量,如果没有,我们不能改变目录到指定目录(在大多数其他shell中,需要改变目录到根目录下)。如果后面有变量arg[1](存储到路径中),我们可以使用os.Chdir(args[1])改变目录。在这个程序块的最后,我们返回execInput 函数以停止进一步的内键命令执行。

由于这很简单,我们只需要在cd块的右面增加一个 built-in exit 命令,就可以停止我们的shell(另一个选择是使用CTRL-C)

// Split the input to separate the command and the arguments.

args := strings.Split(input, " ")

// Check for built-in commands.

switch args[0] {

case "cd":

// 'cd' to home dir with empty path not yet supported.

if len(args) < 2 {

return errors.New("path required")

     }

err := os.Chdir(args[1])

if err != nil {

return err

}

// Stop further processing.

return nil

case "exit":

os.Exit(0)

}

当然,随后的输出看起来像我的跟目录了。

> cd /

> ls

Applications

Library

Network

System

综上,我们写了一个简单的shell。

可以考虑的优化

当你不满足于这些时,你可以试着提升你的 shell 。这有些灵感:

  • 修改输入指标:

  • 增加工作目录

  • 增加机器主机名

  • 增加当前用户

  • 通过上 / 下键,浏览你的输入历史

已经到了文章的结尾,希望你们享受这个过程。我认为,当你理解了 shell 背后的概念, shell 真的相当简单。

Go 语言也是更简单的编程语言之一,他帮助我们更快的得到结果。 Go 语言可以通过自身管理内存,无需我们做任何低级别的操作。 Rob Pike 和 Ken Thompson 与创建 Unix 的 Robert Griesemer 共同创建了 Go 语言,所以我想用 Go 语言写一个 shell 是个很好的组合。

因为我也是在学习中,如果你发现了一些可以提升的东西请联系我。

后续更新

根据新闻网站Reddit的评论( https:/ /www.reddit.com/r/golang/comments/8vj47z/writing_a_simple_shell_in_go / ),我现在已经使用了正确的输出设备。

完整的源代码

下面是完整的源代码,你可以查看这个仓库( https:/ /gitlab.com/sj14/gosh/ ),但源代码有可能已经与本文中展示的代码有所不同。

package main

import (

"bufio"

"errors"

"fmt"

"os"

"os/exec"

"strings"

)

func main() {

reader := bufio.NewReader(os.Stdin)

for {

     fmt.Print("> ")

     // Read the keyboad input.

     input, err := reader.ReadString('\n')

     if err != nil {

          fmt.Fprintln(os.Stderr, err)

     }

     // Handle the execution of the input.

     err = execInput(input)

     if err != nil {

          fmt.Fprintln(os.Stderr, err)

     }

}

}

// ErrNoPath is returned when 'cd' was called without a second argument.

var ErrNoPath = errors.New("path required")

func execInput(input string) error {

// Remove the newline character.

input = strings.TrimSuffix(input, "\n")

// Split the input separate the command and the arguments.

args := strings.Split(input, " ")

// Check for built-in commands.

switch args[0] {

     case "cd":

          // 'cd' to home with empty path not yet supported.

          if len(args) < 2 {

               return ErrNoPath

          }

          err := os.Chdir(args[1])

          if err != nil {

               return err

          }

          // Stop further processing.

          return nil

     case "exit":

          os.Exit(0)

}

// Prepare the command to execute.

cmd := exec.Command(args[0], args[1:]...)

// Set the correct output device.

cmd.Stderr = os.Stderr

cmd.Stdout = os.Stdout

// Execute the command and save it's output.

err := cmd.Run()

if err != nil {

     return err

}

return nil

}

参考资料

原文链接: https://sj14.gitlab.io/post/2018-07-01-go-unix-shell/

原文作者: Simon   Jürgensmeyer

[译] 用 Go 语言写一个简单的 shell

查看原文: [译] 用 Go 语言写一个简单的 shell

  • yellowbird
  • orangerabbit
  • whitetiger
需要 登录 后回复方可回复, 如果你还没有账号你可以 注册 一个帐号。