Golang中exec.Cmd的Stdin管道与文件输入对比

Golang中exec.Cmd的Stdin管道与文件输入对比 大家好,

我在向 exec.Command 提供缓冲区时注意到一些意外行为。

有一个用于创建图像缩略图的工具,叫做“vipsthumbnail”,我在一个类似这样的流水线架构中使用它:

vipsthumbnail stdin -o thumbnail.webp < original.jpeg

它能按预期创建缩略图。

Go 语言的等效实现也以相同的方式工作:

original, err := os.Open("original.jpeg")
if err != nil {
	log.Fatal(err)
}

cmd := exec.Command("vipsthumbnail", "stdin", "-o", "thumbnail.webp")
cmd.Stdin = original
if err := cmd.Run(); err != nil {
	log.Fatal(err)
}

然而,使用缓冲区却不行:

original, err := os.ReadFile("original.jpeg")
if err != nil {
	log.Fatal(err)
}

cmd := exec.Command("vipsthumbnail", "stdin", "-o", "thumbnail.webp")
cmd.Stdin = bytes.NewReader(original)
if err := cmd.Run(); err != nil {
	log.Fatal(err)
}

使用缓冲区时,vipsthumbnail 工具会失败,并显示一些通用的“无法从标准输入读取”消息。

从 Go 文档中我们了解到:

如果 Stdin 是 *os.File,则进程的标准输入直接连接到该文件。

否则,在命令执行期间,一个单独的 goroutine 会从 Stdin 读取数据,并通过管道将该数据传递给命令。

这是 vipsthumbnail 软件中的一个实现错误,导致它无法正确处理管道吗?这对该软件究竟有何不同?在我的 Go 代码中有什么可以做的吗?

感谢所有的帮助!


更多关于Golang中exec.Cmd的Stdin管道与文件输入对比的实战教程也可以访问 https://www.itying.com/category-94-b0.html

5 回复

我尝试使用 vipsthumbnail 作为示例,以便更好地理解这个问题。

这个问题是关于在 exec.Command 中传递缓冲区与使用文件句柄的区别。根据我的理解,使用缓冲区不应该影响命令调用的行为。

更多关于Golang中exec.Cmd的Stdin管道与文件输入对比的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


你尝试过使用这个Go语言包吗?

GitHub

GitHub - davidbyttow/govips: 一个闪电般快速的图像处理和调整大小库…

一个闪电般快速的图像处理和调整大小库,适用于Go语言 - GitHub - davidbyttow/govips: 一个闪电般快速的图像处理和调整大小库,适用于Go语言

感谢你测试这个!我原本期望它能像你说的那样工作。

经过更多测试,我可以确认这是 vipsthumbnail 的问题,并且它只在 Windows 上失败。

以下命令在 Windows 和 Linux 上都能工作

vipsthumbnail stdin -o thumbnail.webp < "image.jpg"

以下命令在 Windows 上失败(在 Windows 上使用 type 代替 cat):

cat "image.jpg" | vipsthumbnail stdin -o thumbnail.webp

同样的命令在 Windows PowerShell 中也失败

Get-Content -Path "image.jpg" -AsByteStream | vipsthumbnail stdin -o thumbnail.webp

感谢你的测试。

由于这显然是 vipsthumbnail 的问题,我现在将关闭这个主题。

我最初的想法是这可能与vipsthumbnail本身有关。我没有安装vipsthumbnail,所以我尝试将你的思路/代码应用到grep上,看起来cmd.Exec的行为符合预期。

我运行了以下代码(我尽量保持你代码/问题的原意),得到了如下结果:

With file:         "Hello, 世界\n"
With bytes reader: "Hello, 世界\n"
func main() {
	f, _ := os.Create("input.txt")
	f.WriteString("Hello, 世界")
	f.Close()

	withFile()
	withStdin()
}

func run(r io.Reader) []byte {
	cmd := exec.Command("grep", "-i", "hello")
	cmd.Stdin = r
	out, err := cmd.Output()
	if err != nil {
		panic(err)
	}
	return out
}

func withFile() {
	f, _ := os.Open("input.txt")
	fmt.Printf("With file:         %q\n", run(f))
}

func withStdin() {
	b, _ := os.ReadFile("input.txt")
	fmt.Printf("With bytes reader: %q\n", run(bytes.NewReader(b)))
}

你尝试过其他命令行程序吗?

这是一个关于 exec.Cmd 标准输入处理的重要问题。问题确实出在 os.Filebytes.Reader 之间的差异上。

cmd.Stdin*os.File 时,Go 直接将文件描述符传递给子进程,子进程可以直接从文件中读取。而当使用 bytes.Reader 时,Go 会创建一个管道,并在单独的 goroutine 中从 reader 复制数据到管道。

对于某些需要随机访问或文件大小检测的工具来说,这种差异很重要。vipsthumbnail 可能依赖于文件描述符的特性。

以下是几种解决方案:

方案1:使用临时文件(最可靠)

original, err := os.ReadFile("original.jpeg")
if err != nil {
    log.Fatal(err)
}

// 创建临时文件
tmpFile, err := os.CreateTemp("", "vips-*.jpeg")
if err != nil {
    log.Fatal(err)
}
defer os.Remove(tmpFile.Name())

// 写入数据
if _, err := tmpFile.Write(original); err != nil {
    log.Fatal(err)
}
tmpFile.Close()

// 重新打开文件用于读取
file, err := os.Open(tmpFile.Name())
if err != nil {
    log.Fatal(err)
}
defer file.Close()

cmd := exec.Command("vipsthumbnail", "stdin", "-o", "thumbnail.webp")
cmd.Stdin = file
if err := cmd.Run(); err != nil {
    log.Fatal(err)
}

方案2:使用 io.Pipe 和 goroutine

original, err := os.ReadFile("original.jpeg")
if err != nil {
    log.Fatal(err)
}

cmd := exec.Command("vipsthumbnail", "stdin", "-o", "thumbnail.webp")
stdinPipe, err := cmd.StdinPipe()
if err != nil {
    log.Fatal(err)
}

// 在 goroutine 中写入数据
go func() {
    defer stdinPipe.Close()
    if _, err := io.Copy(stdinPipe, bytes.NewReader(original)); err != nil {
        log.Printf("写入管道错误: %v", err)
    }
}()

if err := cmd.Run(); err != nil {
    log.Fatal(err)
}

方案3:检查工具是否支持其他输入方式 有些图像处理工具支持从标准输入读取但需要特定格式:

original, err := os.ReadFile("original.jpeg")
if err != nil {
    log.Fatal(err)
}

cmd := exec.Command("vipsthumbnail", "-", "-o", "thumbnail.webp")
cmd.Stdin = bytes.NewReader(original)
if err := cmd.Run(); err != nil {
    log.Fatal(err)
}

这不是 vipsthumbnail 的实现错误,而是许多命令行工具在处理标准输入时的常见行为差异。文件描述符提供了更多元数据(如文件大小、seek 能力),而管道是顺序流。

对于图像处理工具,通常推荐使用临时文件方案,因为它最接近原生文件操作,避免了管道可能带来的缓冲问题。

回到顶部