33

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

 5 years ago
source link: https://mp.weixin.qq.com/s/9pGt1kvoCZhXk9gUxdOhzQ?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文我们将用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

bMVrI3M.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK