在 Golang 中执行 Shell 命令

原文标题:Executing Shell Commands in Golang

作者:Soham Kamani

之前自己也写过 os/exec 包执行 Shell 命令的文章,但是没有这篇讲的详细,感兴趣可以看看,点此处

在本教程中,我们将学习如何在 Golang 中执行shell命令(如 lsmkdirgrep )。我们还将学习如何通过 stdinstdout 传递 I/O 到正在运行的命令,以及管理长时间运行的命令。

如果只是想看代码,可以在 Github 上查看

Exec

我们可以使用官方的 os/exec 包来运行外部命令。

当我们执行 shell 命令时,我们是在 Go 应用程序之外运行代码。为此,我们需要在子进程中运行这些命令。

child-process-thread.drawio.svg

每个命令都作为正在运行的 Go 应用程序中的子进程运行,并公开我们可以用来从进程读取和写入数据的 StdinStdout 属性。

运行基本的 Shell 命令

要运行一个简单的命令并读取其输出,我们可以创建一个新的 *exec.Cmd 实例并运行它。在此示例中,让我们使用 ls 列出当前目录中的文件,并打印代码的输出:

// 创建了一个新的 *Cmd 实例
// 使用 "ls" 命令和 "./" 参数作为参数
cmd := exec.Command("ls", "./")

// 使用 `Output` 方法执行该命令并收集其输出
out, err := cmd.Output()
if err != nil {
  // 如果执行命令时出现错误,则输出错误信息
  fmt.Println("could not run command: ", err)
}
// 否则,输出运行该命令的输出结果
fmt.Println("Output: ", string(out))

由于我在示例仓库中运行此代码,因此它会打印项目根目录中的文件:

> go run shellcommands/main.go

Output:  LICENSE
README.md
go.mod
shellcommands

cmd-output-execution.drawio.svg

请注意,当我们运行 exec 时,我们的应用程序不会生成 shell,而是直接运行给定的命令。这意味着将不会执行任何基于 shell 的处理,例如 glob 模式或扩展。

执行持久运行的命令

前面的示例执行了 ls 命令,该命令立即返回了它的输出。那些输出是连续的,或者需要很长时间才能检索的命令呢?

例如,当我们运行 ping 命令时,我们会定期获得连续输出:

➜  ~ ping google.com
PING google.com (142.250.77.110): 56 data bytes
64 bytes from 142.250.77.110: icmp_seq=0 ttl=116 time=11.397 ms
64 bytes from 142.250.77.110: icmp_seq=1 ttl=116 time=17.646 ms  ## this is received after 1 second
64 bytes from 142.250.77.110: icmp_seq=2 ttl=116 time=10.036 ms  ## this is received after 2 seconds
64 bytes from 142.250.77.110: icmp_seq=3 ttl=116 time=9.656 ms   ## and so on
# ...

如果我们尝试使用 cmd.Output 执行此类型的命令,我们将不会得到任何输出,因为 Output 方法等待命令执行,而 ping 命令执行时间无限。

相反,我们可以使用自定义的 Stdout 属性来连续读取输出:

cmd := exec.Command("ping", "google.com")

// pipe the commands output to the applications
// standard output
cmd.Stdout = os.Stdout

// Run still runs the command and waits for completion
// but the output is instantly piped to Stdout
if err := cmd.Run(); err != nil {
  fmt.Println("could not run command: ", err)
}

这段代码使用 Go 语言的 exec 包来执行 ping 命令并将输出重定向到标准输出流(os.Stdout)。具体来说,它创建了一个命令对象(cmd),该对象包含要执行的命令(“ping"和"google.com”)。然后将命令的标准输出流(cmd.Stdout)设置为应用程序的标准输出流(os.Stdout)。最后,使用 cmd.Run() 方法运行该命令,并等待其完成。如果运行命令时出现错误,将在控制台输出错误信息。

输出结果:

> go run shellcommands/main.go

PING google.com (142.250.195.142): 56 data bytes
64 bytes from 142.250.195.142: icmp_seq=0 ttl=114 time=9.397 ms
64 bytes from 142.250.195.142: icmp_seq=1 ttl=114 time=37.398 ms
64 bytes from 142.250.195.142: icmp_seq=2 ttl=114 time=34.050 ms
64 bytes from 142.250.195.142: icmp_seq=3 ttl=114 time=33.272 ms

# ...
# and so on

通过直接分配 Stdout 属性,我们可以捕获整个命令生命周期的输出,并在收到后立即处理。

cmd-stdout-workflow.drawio.svg

自定义输出写入程序

与使用 os.Stdout 不同,我们可以创建实现 io.Writer 接口的自己的编写器。

让我们创建一个编写器,在每个输出块之前添加一个 "received output:" 前缀:

type customOutput struct{}

func (c customOutput) Write(p []byte) (int, error) {
	fmt.Println("received output: ", string(p))
	return len(p), nil
}

现在我们可以指定一个新的 customWriter 实例作为输出写入器:

cmd.Stdout = customOutput{}

如果我们现在运行应用程序,我们将得到以下输出:

received output:  PING google.com (142.250.195.142): 56 data bytes
64 bytes from 142.250.195.142: icmp_seq=0 ttl=114 time=187.825 ms

received output:  64 bytes from 142.250.195.142: icmp_seq=1 ttl=114 time=19.489 ms

received output:  64 bytes from 142.250.195.142: icmp_seq=2 ttl=114 time=117.676 ms

received output:  64 bytes from 142.250.195.142: icmp_seq=3 ttl=114 time=57.780 ms

使用 STDIN 将输入传递给命令

在前面的示例中,我们在不提供任何输入(或提供有限的输入作为参数)的情况下执行命令。在大多数情况下,输入是通过 STDIN 流给出的。

译注:就是外部给命令,然后去执行

一个著名的例子是 grep 命令,我们可以通过管道从另一个命令输入:

➜  ~ echo "1. pear\n2. grapes\n3. apple\n4. banana\n" | grep apple
3. apple

在这里,输入通过 STDIN 传递给 grep 命令。在本例中,输入是一个水果列表,grep 过滤包含 " apple" 的行。

Cmd 实例为我们提供了一个可以写入的输入流。让我们使用它向 grep 子进程传递输入:

cmd := exec.Command("grep", "apple")

// Create a new pipe, which gives us a reader/writer pair
reader, writer := io.Pipe()
// assign the reader to Stdin for the command
cmd.Stdin = reader
// the output is printed to the console
cmd.Stdout = os.Stdout

go func() {
  defer writer.Close()
  // the writer is connected to the reader via the pipe
  // so all data written here is passed on to the commands
  // standard input
  writer.Write([]byte("1. pear\n"))
  writer.Write([]byte("2. grapes\n"))
  writer.Write([]byte("3. apple\n"))
  writer.Write([]byte("4. banana\n"))
}()

if err := cmd.Run(); err != nil {
  fmt.Println("could not run command: ", err)
}

输出:

3. apple

stdin.drawio.svg

Kill 一个子进程

有几个命令会无限期地运行,或者需要明确的信号才能停止。

例如,如果我们使用 python3 -m http.server 启动 Web 服务器或执行 sleep 10000,则生成的子进程将运行很长时间(或无限运行)。

要停止这些进程,我们需要从应用程序发送终止信号。我们可以通过向命令添加一个上下文实例来做到这一点。

如果上下文被取消,命令也会终止。

ctx := context.Background()
// The context now times out after 1 second
// alternately, we can call `cancel()` to terminate immediately
ctx, cancel = context.WithTimeout(ctx, 1*time.Second)

cmd := exec.CommandContext(ctx, "sleep", "100")

out, err := cmd.Output()
if err != nil {
  fmt.Println("could not run command: ", err)
}
fmt.Println("Output: ", string(out))

这将在 1 秒后给出以下输出:

could not run command:  signal: killed
Output:  

当您想要限制运行命令所花费的时间或想要创建回退以防命令未按时返回结果时,终止子进程很有用。

总结

到目前为止,我们学习了多种执行 unix shell 命令并与之交互的方法。使用 os/exec 包时需要注意以下几点:

  • 当您想要执行通常不会提供太多输出的简单命令时,请使用 cmd.Output
  • 对于具有连续或长时间运行输出的函数,您应该使用 cmd.Run 并使用 cmd.Stdoutcmd.Stdin 与命令交互
  • 在生产应用程序中,如果某个进程在给定的时间内没有响应,那么保持超时并终止该进程是非常有用的。我们可以使用上下文取消发送终止命令。

如果您想了解更多关于不同功能和配置选项的信息,可以查看官方文档页面

您可以在 Github 上查看所有示例的工作代码。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 你可以使用 `os/exec` 包来在 Go 实现与 Shell 的交互。下面是一个简单的示例代码: ```go package main import ( "bufio" "fmt" "os/exec" ) func main() { cmd := exec.Command("sh") stdin, err := cmd.StdinPipe() if err != nil { panic(err) } stdout, err := cmd.StdoutPipe() if err != nil { panic(err) } scanner := bufio.NewScanner(stdout) if err := cmd.Start(); err != nil { panic(err) } go func() { defer stdin.Close() stdin.Write([]byte("echo hello world\n")) }() for scanner.Scan() { fmt.Println(scanner.Text()) } if err := cmd.Wait(); err != nil { panic(err) } } ``` 这个例子创建了一个 `sh` 进程,并通过管道向其输入了一个命令 `echo hello world`,然后读取了输出并打印到控制台。你可以根据自己的需求修改这个例子。 ### 回答2: golangShell交互是指在Golang程序执行Shell命令并与其进行交互的功能。 在Golang实现Shell交互可以使用os/exec包。该包提供了执行Shell命令的函数和方法,可以通过它们来执行Shell命令并获取命令的标准输出、标准错误和执行结果。 首先,我们需要引入os/exec包。然后,使用exec.Command函数创建一个Cmd类型的对象,该对象表示要执行Shell命令。可以通过Cmd对象的方法设置命令的参数、工作目录等。 接下来,使用Cmd对象的CombinedOutput方法来执行Shell命令,并获取命令的输出结果。CombinedOutput方法会返回一个字节数组,其存储了命令的标准输出和标准错误。 最后,我们可以将字节数组转换为字符串,并进行相应的处理,如打印输出或进一步解析。 以下是一个简单的示例代码,演示了如何在Golang实现Shell交互的功能: ``` package main import ( "fmt" "log" "os/exec" ) func main() { cmd := exec.Command("ls", "-l") // 创建Cmd对象,执行ls -l命令 output, err := cmd.CombinedOutput() // 执行命令并获取输出结果 if err != nil { log.Fatal(err) // 如果执行命令出错,打印错误信息并退出 } fmt.Println(string(output)) // 将字节数组转换为字符串并打印输出 } ``` 以上代码会执行`ls -l`命令,并将结果打印到控制台。 需要注意的是,在使用Golang执行Shell命令时,需要保证可执行命令位于环境变量`$PATH`所指定的路径,否则需要指定完整的命令路径。示例的`ls`命令是一个常见的Shell命令,因此不需要指定路径。 总结来说,通过使用os/exec包,我们可以在Golang程序实现Shell交互的功能,从而执行Shell命令,并获取其输出结果。 ### 回答3: Go语言本身并不提供直接的Shell交互功能,但我们可以使用一些第三方库来实现类似的功能。 一个常用的库是“os/exec”,它允许我们执行外部 Shell 命令并与其进行交互。下面是一个简单的示例代码: ```go package main import ( "bufio" "fmt" "os" "os/exec" ) func main() { // 创建一个命令对象 cmd := exec.Command("/bin/sh") // 获取命令的标准输入和输出管道 stdin, _ := cmd.StdinPipe() stdout, _ := cmd.StdoutPipe() // 启动命令 cmd.Start() // 准备一个输入读取器,用于读取用户输入 reader := bufio.NewReader(os.Stdin) for { // 读取用户输入 input, _ := reader.ReadString('\n') // 将用户输入写入命令的标准输入管道 stdin.Write([]byte(input)) // 读取命令的标准输出 output := make([]byte, 1024) stdout.Read(output) // 打印输出结果 fmt.Println(string(output)) } } ``` 这段代码会启动一个新的Shell,并不断读取用户的输入并将其发送给Shell的标准输入,然后读取Shell的标准输出并打印出来。 请注意,这里的示例代码没有处理错误情况,为了简单起见,省略了错误处理部分。在实际应用,我们应该对错误进行适当处理。 希望这个示例能够帮助你实现Go语言下的Shell交互功能。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值