在GO中编写一个简单的shell

Introduction

In this post, we will write a minimalistic shell for UNIX(-like) operating systems in the Go programming language and it only takes about 60 lines of code. You should be a little bit familiar with Go (e.g. how to build a simple project) and the basic usage of a UNIX shell.

UNIX is very simple, it just needs a genius to understand its simplicity. - Dennis Ritchie

Of course, I’m not a genius, and I’m not even sure if Dennis Ritchie meant to include the userspace tools. Furthermore, a shell is only one small part (and compared to the kernel, it’s really an easy part) of a fully functional operating system, but I hope at the end of this post, you are just as astonished as I was, how simple it is to write a shell once you understood the concepts behind it.

What is a shell?

Definitions are always difficult. I would define a shell as the basic user interface to your operating system. You can input commands to the shell and receive the corresponding output. When you need more information or a better definition, you can look up the Wikipediaarticle.

Some examples of shells are:

  • Bash

  • Zsh

  • Gnome Shell

  • Windows Shell

The graphical user interfaces of Gnome and Windows are shells but most IT related people (or at least I) will refer to a text-based one when talking about shells, e.g. the first two in this list. Of course, this example will describe a simple and non-graphical shell.

In fact, the functionality is explained as: give an input command and receive the output of this command. An example? Run the program lsto list the content of a directory.

Input:

1ls

Output:

1Applications            etc
2Library                home
3...

That’s it, super simple. Let’s start!

The input loop

To execute a command, we have to accept inputs. These inputs are done by us, humans, using keyboards ;-)

The keyboard is our standard input device (os.Stdin) and we can create a reader to access it. Each time, we press the enter key, a new line is created. The new line is denoted by \n. While pressing the enter key, everything written is stored in the variable input.

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

Let’s put this in a main function of our main.go file, and by adding afor loop around the ReadString function, we can input commands continuously. When an error occurs while reading the input, we will print it to the standard error device (os.Stderr). If we would just usefmt.Println without specifying the output device, the error would be directed to the standard output device (os.Stdout). This would not change the functionality of the shell itself, but separate devices allow easy filtering of the output for further processing.

1func main() {
2    reader := bufio.NewReader(os.Stdin)    for {        // Read the keyboad input.
3        input, err := reader.ReadString('\n')        if err != nil {
4            fmt.Fprintln(os.Stderr, err)
5        }
6    }
7}

Executing Commands

Now, we want to execute the entered command. Let’s create a new function execInput for this, which takes a string as an argument. First, we have to remove the newline control character \n at the end of the input.
Next, we prepare the command with exec.Command(input) and assign the corresponding output and error device for this command. Finally, the prepared command is processed with cmd.Run().

1func execInput(input string) error {    // Remove the newline character.
2    input = strings.TrimSuffix(input, "\n")    // Prepare the command to execute.
3    cmd := exec.Command(input)    // Set the correct output device.
4    cmd.Stderr = os.Stderr
5    cmd.Stdout = os.Stdout    // Execute the command and save it's output.
6    err := cmd.Run()    if err != nil {        return err
7    }    return nil
8}

First Prototype

We complete our main function by adding a fancy input indicator (>) at the top of the loop, and by adding the new execInput function at the bottom of the loop.

 1func main() {
 2    reader := bufio.NewReader(os.Stdin)    for {
 3        fmt.Print("> ")        // Read the keyboad input.
 4        input, err := reader.ReadString('\n')        if err != nil {
 5            fmt.Println(err)
 6        }        // Handle the execution of the input.
 7        err = execInput(input)        if err != nil {
 8            fmt.Println(err)
 9        }
10    }
11}

It’s time for a first test run. Build and run the shell with go run main.go. You should see the input indicator > and be able to write something. For example, we could run the ls command.

1> ls
2LICENSE
3main.go
4main_test.go

Wow, it works! The program ls was executed and gave us the content of the current directory. You can exit the shell just like most other programs with the key combination CTRL-C.

Arguments

Let’s get the list in long format with ls -l.

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

It’s not working anymore. This is because our shell tries to run the program ls -l, which is not found. The program is just ls and -l is a so-called argument, which is parsed by the program itself. Currently, we don’t distinguish between the command and the arguments. To fix this, we have to modify the execLine function and split the input on each space.

1func execInput(input string) error {    // Remove the newline character.
2    input = strings.TrimSuffix(input, "\n")    // Split the input to separate the command and the arguments.
3    args := strings.Split(input, " ")    // Pass the program and the arguments separately.
4    cmd := exec.Command(args[0], args[1:]...)
5    ...
6}

The program name is now stored in args[0] and the arguments in the subsequent indexes. Running ls -l now works as expected.

1> ls -l
2total 24
3-rw-r--r--  1 simon  staff  1076 30 Jun 09:49 LICENSE
4-rw-r--r--  1 simon  staff  1058 30 Jun 10:10 main.go
5-rw-r--r--  1 simon  staff   897 30 Jun 09:49 main_test.go

Change Directory (cd)

Now we are able to run commands with an arbitrary number of arguments. To have a set of functionality which is necessary for a minimal usability, there is only one thing left (at least according to my opinion). You might already come across this while playing with the shell: you can’t change the directory with the cd command.

1> cd /
2> ls
3LICENSE
4main.go
5main_test.go

No, this is definitely not the content of my root directory. Why does the cd command not work? When you know, it’s easy: there is no realcd program, the functionality is a built-in command of the shell.

Again, we have to modify the execInput function. Just after the Splitfunction, we add a switch statement on the first argument (the command to execute) which is stored in args[0]. When the command is cd, we check if there are subsequent arguments, otherwise, we can not change to a not given directory (in most other shells, you would then change to your home directory). When there is a subsequent argument in args[1] (which stores the path), we change the directory with os.Chdir(args[1]). At the end of case block, we return theexecInput function to stop further processing of this built-in command.
Because it is so simple, we will just add a built-in exit command right below the cd block, which stops our shell (an alternative to usingCTRL-C).

1// 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.
2    if len(args) < 2 {        return  errors.New("path required")
3    }
4    err := os.Chdir(args[1])    if err != nil {        return err
5    }    // Stop further processing.
6    return nilcase "exit":
7    os.Exit(0)
8}
9...

Yes, the following output looks more like my root directory.

1> cd /
2> ls
3Applications
4Library
5Network
6System
7...

That’s it. We have written a simple shell :-)

Considered improvements

When you are not already bored by this, you can try to improve your shell. Here is some inspiration:

  • Modify the input indicator:

    • add the working directory

    • add the machine’s hostname

    • add the current user

  • Browse your input history with the up/down keys

Conclusion

We reached the end of this post and I hope you enjoyed it. I think, when you understand the concepts behind it, it’s quite simple.

Go is also one of the more simple programming languages, which helped us to get to the results faster. We didn’t have to do any low-level stuff as managing the memory ourselves. Rob Pike and Ken Thompson, who created Go together with Robert Griesemer, also worked on the creation of UNIX, so I think writing a shell in Go is a nice combination.

As I’m always learning too, please just contact me whenever you find something which should be improved.

Updated 03.07.2018

Based on the reddit comments, I’m now using the correct output devices.

Complete Source-Code

Below is the full source-code. You can also check the repository, but the code there might already have diverged from the code presented in this post.

 1package main
 2
 3import (    "bufio"
 4    "errors"
 5    "fmt"
 6    "os"
 7    "os/exec"
 8    "strings")
 9
10func main() {
11    reader := bufio.NewReader(os.Stdin) for {
12        fmt.Print("> ")     // Read the keyboad input.
13        input, err := reader.ReadString('\n')       if err != nil {
14            fmt.Fprintln(os.Stderr, err)
15        }       // Handle the execution of the input.
16        err = execInput(input)      if err != nil {
17            fmt.Fprintln(os.Stderr, err)
18        }
19    }
20}// ErrNoPath is returned when 'cd' was called without a second argument.var ErrNoPath = errors.New("path required")
21
22func execInput(input string) error {    // Remove the newline character.
23    input = strings.TrimSuffix(input, "\n") // Split the input separate the command and the arguments.
24    args := strings.Split(input, " ")   // Check for built-in commands.
25    switch args[0] {    case "cd":      // 'cd' to home with empty path not yet supported.
26        if len(args) < 2 {          return ErrNoPath
27        }
28        err := os.Chdir(args[1])        if err != nil {         return err
29        }       // Stop further processing.
30        return nil
31    case "exit":
32        os.Exit(0)
33    }   // Prepare the command to execute.
34    cmd := exec.Command(args[0], args[1:]...)   // Set the correct output device.
35    cmd.Stderr = os.Stderr
36    cmd.Stdout = os.Stdout
37
38    // Execute the command and save it's output.
39    err := cmd.Run()    if err != nil {     return err
40    }   return nil}

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
编写一个简单的常驻内存的Windows服务,你可以使用`github.com/kardianos/service`包。下面是一个示例代码: ```go package main import ( "fmt" "log" "os" "time" "github.com/kardianos/service" ) var logger service.Logger type program struct{} func (p *program) Start(s service.Service) error { go p.run() return nil } func (p *program) run() { for { select { case <-time.Tick(1 * time.Second): fmt.Println("Service is running...") } } } func (p *program) Stop(s service.Service) error { return nil } func main() { svcConfig := &service.Config{ Name: "MyService", DisplayName: "My Service", Description: "This is a simple Windows service.", } prg := &program{} s, err := service.New(prg, svcConfig) if err != nil { log.Fatal(err) } logger, err = s.Logger(nil) if err != nil { log.Fatal(err) } if len(os.Args) > 1 { err = service.Control(s, os.Args[1]) if err != nil { log.Fatal(err) } return } err = s.Run() if err != nil { logger.Error(err) } } ``` 在这个示例,我们创建了一个`program`结构体来实现Windows服务的逻辑。在`Start`方法,我们通过一个无限循环来模拟服务的常驻内存。你可以在循环添加自己的逻辑。 在`main`函数,我们首先定义了服务的配置,包括服务的名称、显示名称和描述。然后创建了一个`program`实例,并使用`service.New`函数创建了一个服务实例。接下来,我们判断命令行参数,如果有参数,则根据参数来控制服务的状态(如启动、停止、重启)。如果没有参数,则调用`s.Run()`方法来启动服务。 通过编译这个代码并将生成的可执行文件安装为Windows服务,你就可以在后台运行这个常驻内存的服务了。可以使用以下命令来安装和管理服务: ```shell # 安装服务 myService.exe install # 启动服务 myService.exe start # 停止服务 myService.exe stop # 重启服务 myService.exe restart # 卸载服务 myService.exe uninstall ``` 请注意,安装和管理服务需要管理员权限。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值