前言

本文是参考Writing a simple shell in Go所实现的一个 toy-demo ,主要用于本人的一个留档记录。

开始实践

获取输入

键盘是我们的标准输入设备,通过创建一个 reader 来访问它。而当我们使用键盘输入命令后,需要通过回车键去确认执行。为了获取我们输入的命令,我们通过 ReadString 方法去获取以 \n 结尾的行内容。

1
2
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')

执行命令

在获取到输入的命令后,我们需要执行这条命令。我们通过设置一个 execInput 函数去执行具体的处理逻辑,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func execInput(input string) error {
// 移除 \n 后缀
input = strings.TrimSuffix(input, "\n")

// 准备执行命令
cmd := exec.Command(input)

// 输出设置
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout

// 执行命令,返回错误
return cmd.Run()
}

初步实现命令行

通过在 main 方法中设置 for 循环使得命令行可以持续工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
func main() {
reader := bufio.NewReader(os.Stdin)

for {
// 输入提示符
fmt.Printf("> ")

// 获取输入的命令
input, err := reader.ReadString('\n')
if err != nil {
fmt.Fprintln(os.Stderr, err)
}

// 执行命令
if err = execInput(input); err != nil {
fmt.Fprintln(os.Stderr, err)
}
}
}

func execInput(input string) error {
// 移除 \n 后缀
input = strings.TrimSuffix(input, "\n")

// 准备执行命令
cmd := exec.Command(input)

// 输出设置
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout

// 执行命令,返回错误
return cmd.Run()
}

自定义命令行提示符

go 提供了三个方法去获取相关的内容。

  • 获取机器名称:
    hostname, err := os.Hostname()
  • 获取当前用户名称:
    currentUser, err := exec.Command("whoami").Output()
  • 获取当前工作目录:
    cwd, err := os.Getwd()

获取相关信息后自行进行拼接即可。

命令参数

当前的命令行并不支持执行携带参数的命令,因为我们现在的程序会将命令与参数当成一个整体去执行,这当然会出现问题。因此就需要我们在 execInput 内进行进一步的优化实现,具体如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func execInput(input string) error {
// 移除 \n 后缀
input = strings.TrimSuffix(input, "\n")

// 将命令与参数进行区分
args := strings.Split(input, " ")

// 准备执行命令
cmd := exec.Command(args[0], args[1:]...)

// 输出设置
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout

// 执行命令,返回错误
return cmd.Run()
}

在 args[0] 内存放着我们的命令,而 args[1:] 存放着相关的参数。

内置命令

在我们执行部分命令,如 cd 命令时,我们会发现我们无法正确执行它,因为这种命令是 shell 的内置命令,我们无法直接通过 $PATH 去访问到它的具体执行程序,这就需要我们自己在 execInput 函数内实现,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func execInput(input string) error {
// 移除 \n 后缀
input = strings.TrimSuffix(input, "\n")

// 将命令与参数进行区分
args := strings.Split(input, " ")

// 检查是否是内置命令
switch args[0] {
case "cd":
// 注意,不支持直接 cd
if len(args) < 2 {
return errors.New("path required")
}
return os.Chdir(args[1])
case "exit":
os.Exit(0)
}

// 准备执行命令
cmd := exec.Command(args[0], args[1:]...)

// 输出设置
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout

// 执行命令,返回错误
return cmd.Run()
}

完整示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package main

import (
"bufio"
"errors"
"fmt"
"os"
"os/exec"
"strings"
)

func main() {
reader := bufio.NewReader(os.Stdin)

// 获取机器名称
hostname, err := os.Hostname()
if err != nil {
panic(err)
}
// 获取当前用户
currentUser, err := exec.Command("whoami").Output()
if err != nil {
panic(err)
}

for {
// 获取当前工作目录
cwd, err := os.Getwd()
if err != nil {
fmt.Fprintln(os.Stderr, err)
}

// 输入提示符
fmt.Printf("%s@%s~%s> ", hostname, currentUser, cwd)

// 获取输入的命令
input, err := reader.ReadString('\n')
if err != nil {
fmt.Fprintln(os.Stderr, err)
}

// 执行命令
if err = execInput(input); err != nil {
fmt.Fprintln(os.Stderr, err)
}
}
}

func execInput(input string) error {
// 移除 \n 后缀
input = strings.TrimSuffix(input, "\n")

// 将命令与参数进行区分
args := strings.Split(input, " ")

// 检查是否是内置命令
switch args[0] {
case "cd":
// 注意,不支持直接 cd
if len(args) < 2 {
return errors.New("path required")
}
return os.Chdir(args[1])
case "exit":
os.Exit(0)
}

// 准备执行命令
cmd := exec.Command(args[0], args[1:]...)

// 输出设置
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout

// 执行命令,返回错误
return cmd.Run()
}