共计 3929 个字符,预计需要花费 10 分钟才能阅读完成。
翻译自:https://sj14.gitlab.io/post/2…
介绍
在本文中,咱们将应用 Go 语言,编写一个最小的 UNIX(-like)操作系统 SHELL,它只须要大略 60 行代码。你须要略微理解一些 Go 语言(晓得如何编译简略的我的项目),以及简略应用 UNIX Shell。
UNIX 非常简单,简略到一个蠢才都能了解它的简略性 – Dennis Ritchie
当然,我并非蠢才,我也不太确定 Dennis Ritchie 所说的,是否也包含运行于用户空间的工具。Shell 只是残缺操作系统的一小部分(相较于内核,它真的是一个简略的局部),但我心愿在本文的结尾,你能够感到吃惊,吃惊于编写一个 SHELL,所用到的常识如此少。
什么是 SHELL
给 SHELL 下定义有点艰难。我认为 SHELL 能够了解为你所应用的操作系统,根本的用户界面。你能够在 SHELL 中输出命令,而后接管一些反馈输入。如果想理解更多信息,或者更明确的定义,请查阅 维基百科 )。
一些 SHELL 的例子:
- Bash)
- Zsh
- Gnome Shell
- Windows Shell
有像 Windows 和 GNOME 这种图形界面 SHELL,但大多数 IT 相干人员(至多我是),当议论起 SHELL,指的是基于文本的 SHELL(下面列表的头两项)。当然,也能够简化的定义为非图形界面 SHELL。
事实上,SHELL 的性能能够定义为输出命令,而后接管该命令的输入。想看个例子?运行 ls
命令,输入目录的内容。
Input:
ls
Output:
Applications etc
Library home
...
就是这样,非常简略。让咱们开始吧!
输出循环
要执行一个命令,咱们必须接管输出。而输出来自咱们人类,应用键盘进行的。
键盘是咱们的规范输出设施(os.Stdin),咱们能够拜访并读取它。当咱们按下回车键的时候,会创立新的一行。这行新的文本以 \n
结尾。当敲击回车键的时候,所有存储在输入区的内容将被输出。
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
让咱们将这些代码输出进咱们的 main.go
文件,ReadingString 办法被嵌套在 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
的新的函数,他接管输出的字符串作为参数。首先,咱们移除输出结尾的换行符。接下来,通过 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 return the error.
return cmd.Run()}
原型
接着,在循环语句下面,增加一个丑化作用的指示器(>),在循环语句上面,增加新的 execInput
函数,此时,次要性能就实现了。
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.
if err = execInput(input); err != nil {fmt.Fprintln(os.Stderr, err)
}
}
}
是时候执行一次测试了。应用 go run main.go
构建并运行咱们的 SHELL。你将看到输出标识符 >
,此时能够承受输出。举个例子,咱们能够执行 ls
命令。
> ls
LICENSE
main.go
main_test.go
不错,能够运行!咱们的程序此时能够执行 ls
命令,并输入当前目录的内容。你能够像退出其余程序一样,应用 ctrl+c
,退出它。
参数
让咱们命令前面加个参数,如 ls -l
。
> ls -l
执此时执行会报错:exec: "ls -l": executable file not found in $PATH
。
这是因为咱们的 SHELL 尝试执行 ls -l
,然而并没有找到叫这个名字的程序。咱们的意思是执行 ls
,带上 -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
命令不起作用呢?要了解这点很容易:没有真正的 cd
程序,该性能是 SHELL
的内置命令。
咱们必须对 execInput
函数再次进行批改。在 Split
办法前面,咱们增加 switch
构造语句,并将 args[0] 作为它的参数。当这个命令是 cd
,咱们查看它前面是否还有参数,如果没有指定参数,咱们无奈扭转当前目录(在大多数 SHELL 中,不指定参数,将跳转到主目录)。当 args[1]
中有一个后续参数时(存储门路的参数),咱们应用 os.Chdir(args[1])
更改目录。在 case
块的开端,咱们返回 execInput
函数以进行其余解决。
因为如此简略,咱们在 cd
块前面,再增加一个 exit
命令,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")
}
// Change the directory and return the error.
return os.Chdir(args[1])
case "exit":
os.Exit(0)
}
...
能够看到,此时输入的内容,相较于之前的输入后果,更像是咱们的根目录。
> cd /
> ls
Applications
Library
Network
System
...
至此,咱们曾经实现了这个简略的 SHEEL 的编写。
思考改善的中央
此时,如果你感觉有些无聊,你能够尝试改良这个 SHELL。上面是一些能够改善的点:
-
批改光标所在行的显示:
- 减少当前目录
- 减少机器名称
- 减少以后用户
- 通过输出 up/down 键,来翻阅输出的历史
结尾
至此,本文已靠近序幕,我心愿你读的欢快。如你所见,SHELL 背地的概念非常简略。
Go 同样是一门非常简略的编程语言,它帮忙咱们更快的失去想要的后果。咱们无需关怀内存治理。Rob Pike 和 Ken Thompson,以及 Robert Griesemer 独特发明了 Go,他们也发明了 Unix,所以,应用 Go 编写 SHELL 是个很好的抉择。
我也始终在学习,如果你发现本文有哪些可改良的中央,请分割我。