用Go语言写一个简单的shell

Posted 容器时代

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了用Go语言写一个简单的shell相关的知识,希望对你有一定的参考价值。



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

        }

    }

}


执行命令


现在,我们想执行输入的命令。让我们为其创建一个新的函数execInputexecInput取一个字符串作为变量。首先,我们在输入的最后删除新行控制字符\n。其次,我们为exec.Commandinput)准备一个命令,并为这个命令设置对应的输出和错误装置。最后,我们准备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 PikeKen Thompson与创建UnixRobert 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/

以上是关于用Go语言写一个简单的shell的主要内容,如果未能解决你的问题,请参考以下文章

用 C 语言写一个简单的 Unix Shell

用Go语言实现一个简单的聊天机器人

如何用 C 语言写一个简单的 Unix Shell

急救:编写一个简单的Shell程序,实现将指定的文件内容中小写字母转换成大写字母显示出来。

Go语言实战 gRPC 实现一个简单微服务

Go语言实战 gRPC 实现一个简单微服务