在Go中无法将Shell命令的输出写入文件

huangapple go评论87阅读模式
英文:

Failure to write output of shell command to file in Go

问题

我已经为执行snt2cooc命令编写了以下函数(这是运行GIZA++的预处理步骤之一)。根据我们的目的,我认为可以将snt2cooc脚本视为黑盒:

func SNTToCOOC(srcVocab, tgtVocab, sntPath, outpath string) error {
    // 打开输出文件进行写入
    outfile, err := os.Create(outpath)
    if err != nil {
        return err
    }
    defer outfile.Close()

    cmdStr := "snt2cooc"
    args := []string{srcVocab, tgtVocab, sntPath}
    cmd := exec.Command(cmdStr, args...)
    cmd.Stdout = outfile
    if err = cmd.Run(); err != nil {
        return err
    }
    cmd.Wait()
    return err
}

运行时,该函数没有报错,但输出文件是空的。这段代码对于其他类似的命令有效,但对于特定的snt2cooc命令无效。我注意到,当我直接在shell中运行该命令时:

snt2cooc file1.vcb file2.vcb file3.snt

我得到以下输出:

END.
0 2
0 3
0 4
0 5
0 6

(为简洁起见进行了截断)

如果我将命令的输出直接发送到文件中:

snt2cooc file1.vcb file2.vcb file3.snt > out.txt

out.txt的内容如预期所示:

0 2
0 3
0 4
0 5
0 6

请注意,在第一种情况下,首先将END.行输出到stdout,然后才将命令的实际输出发送到stdout。因此,我认为存在竞争条件,即Go代码在命令的最终输出写入文件之前就执行完毕了,尽管调用了cmd.Wait()。我不太确定snt2cooc命令在内部具体做了什么。有人可以提供一些提示来解决这个问题吗?

编辑1:

似乎包含500毫秒的延迟的以下代码可以始终将snt2cooc命令的输出写入文件:

cmdStr := "snt2cooc"
args := []string{srcVocab, tgtVocab, sntPath}
cmd := exec.Command(cmdStr, args...)
stdout, err := cmd.StdoutPipe()
time.Sleep(500 * time.Millisecond)
if err != nil {
    return err
}
err = cmd.Start()
if err != nil {
    return err
}

out := bufio.NewScanner(stdout)
for out.Scan() {
    outfile.Write(out.Bytes())
    outfile.WriteString("\n")
}
if err := out.Err(); err != nil {
    return err
}

这向我证明存在某种竞争条件,即Go程序在所有输出写入文件之前就退出了。我在这个问题上添加了一个悬赏,希望有人能够:1)解释为什么会发生这种情况;2)提供一种非hack的方法(即不使用500毫秒的延迟)来解决这个问题。

英文:

I have written the following function for executing the snt2cooc command (one of the preprocessing steps for running GIZA++. For our purposes I think we can consider the snt2cooc script to be a black box):

func SNTToCOOC(srcVocab, tgtVocab, sntPath, outpath string) error {
	// open the out file for writing
	outfile, err := os.Create(outpath)
	if err != nil {
		return err
	}
	defer outfile.Close()

	cmdStr := "snt2cooc"
	args := []string{srcVocab, tgtVocab, sntPath}
	cmd := exec.Command(cmdStr, args...)
	cmd.Stdout = outfile
	if err = cmd.Run(); err != nil {
		return err
	}
	cmd.Wait()
	return err
}

When running, the function executes without an error, but the output file is empty. This same code works for other similar commands, but not for this specific snt2cooc command, and I noticed that when I run this command in the shell directly:

snt2cooc file1.vcb file2.vcb file3.snt

I get the following output:

END.
0 2
0 3
0 4
0 5
0 6

(truncated for brevity)

And if I send the output of the command to a file directly from the shell:

snt2cooc file1.vcb file2.vcb file3.snt > out.txt

The contents of out.txt are as expected:

0 2
0 3
0 4
0 5
0 6

Notice how in the first case, the line END. is output to stdout first, and only then is the real output of the command sent to stdout. I therefore think there is a race condition going on, where the Go code finishes executing before the command's final output is written to file. This despite calling cmd.Wait(). I'm not too sure what exactly the snt2cooc command is doing internally. Could someone provide a hint on how to solve this?

Edit 1:

It seems like the following code, with the sleep of 500ms included, consistently writes output to the file for the snt2cooc command:

cmdStr := "snt2cooc"
args := []string{srcVocab, tgtVocab, sntPath}
cmd := exec.Command(cmdStr, args...)
stdout, err := cmd.StdoutPipe()
time.Sleep(500 * time.Millisecond)
if err != nil {
	return err
}
err = cmd.Start()
if err != nil {
	return err
}

out := bufio.NewScanner(stdout)
for out.Scan() {
	outfile.Write(out.Bytes())
	outfile.WriteString("\n")
}
if err := out.Err(); err != nil {
	return err
}

This proves to me that there is some race condition going on, with the Go program exiting before all output is written to file. I added a bounty to this question, with the hope that someone can 1) explain why this is happening and 2) provide a non-hacky way (i.e. 500ms sleep) to fix it.

答案1

得分: 5

首先,清理你的代码。

cmd.Stderr = os.DevNull,这样你就忽略了标准错误输出。Stdout 和 Stderr 分别指定了进程的标准输出和错误输出。如果其中任何一个为 nil,Run 函数会将相应的文件描述符连接到空设备(os.DevNull)。

cmd.Wait() 返回一个错误,你忽略了它。func (c *Cmd) Wait() error

Wait 函数等待命令退出。它必须由 Start 函数启动。你使用的是 Run 函数,而不是 Start 函数。

当你运行这段代码时,你会得到什么输出?

failure.go:

package main

import (
	"fmt"
	"os"
	"os/exec"
)

func main() {
	err := SNTToCOOC("file1.vcb", "file2.vcb", "file3.snt", "out.txt")
	if err != nil {
		fmt.Println(err)
	}
}

func SNTToCOOC(srcVocab, tgtVocab, sntPath, outpath string) error {
	outfile, err := os.Create(outpath)
	if err != nil {
		return err
	}
	defer outfile.Close()
	cmdStr := "snt2cooc"
	args := []string{srcVocab, tgtVocab, sntPath}
	cmd := exec.Command(cmdStr, args...)
	cmd.Stdout = outfile
	cmd.Stderr = os.Stderr
	err = cmd.Run()
	if err != nil {
		return err
	}
	return err
}

运行:

$ rm -f out.txt && go run failure.go && cat out.txt

此外,当你将 cmd.Stdout = os.Stdout 替换为 cmd.Stdout = outfile 时,你会得到什么输出?

英文:

First, clean up your code.

cmd.Stderr = os.DevNull, so you ignore stderr. Stdout and Stderr specify the process's standard output and error. If either is nil, Run connects the corresponding file descriptor to the null device (os.DevNull).

cmd.Wait() returns error, you ignore it. func (c *Cmd) Wait() error.

Wait waits for the command to exit. It must have been started by Start. You use Run, not Start.

What output do you get when you run this code?

failure.go:

package main

import (
	"fmt"
	"os"
	"os/exec"
)

func main() {
	err := SNTToCOOC("file1.vcb", "file2.vcb", "file3.snt", "out.txt")
	if err != nil {
		fmt.Println(err)
	}
}

func SNTToCOOC(srcVocab, tgtVocab, sntPath, outpath string) error {
	outfile, err := os.Create(outpath)
	if err != nil {
		return err
	}
	defer outfile.Close()
	cmdStr := "snt2cooc"
	args := []string{srcVocab, tgtVocab, sntPath}
	cmd := exec.Command(cmdStr, args...)
	cmd.Stdout = outfile
	cmd.Stderr = os.Stderr
	err = cmd.Run()
	if err != nil {
		return err
	}
	return err
}

Run:

$ rm -f out.txt && go run failure.go && cat out.txt

Also, what output do you get when you run this code with cmd.Stdout = os.Stdout substituting for cmd.Stdout = outfile.

答案2

得分: -1

问题不在于SNTtoCooc的内部,而是在于使用cmd.Stdout将内容写入文件本身的方式:

func anyWrite(args []string, outpath string) error {
        outfile, err := os.Create(outpath)
        if err != nil {
                return err
        }

        defer outfile.Close()

        // 这里我使用简单的 "echo" 命令
        cmd := exec.Command("echo", args...)
        stdout, err := cmd.Output()
        if err != nil {
     	        return err
        }

        // 使用这个方法代替 cmd.Stdout 似乎可以解决问题
        outfile.Write(stdout)
             
        return nil
}

func main() {
        args := []string{"Line 1", "Line 2", "Line 3"}
        if err := anyWrite(args, "./outfile.txt"); err != nil {
                panic(err) 
        }
}

根据os/exec中的注释

Stdout 和 Stderr 指定进程的标准输出和错误输出。如果其中任何一个为 nil,Run 函数会将相应的文件描述符连接到空设备(os.DevNull)。

英文:

The problem isn't with the innard of SNTtoCooc but how you write to the file itself using cmd.Stdout:

func anyWrite(args []string, outpath string) error {
        outfile, err := os.Create(outpath)
        if err != nil {
                return err
        }

        defer outfile.Close()

        // I use simple "echo" here
        cmd := exec.Command("echo", args...)
        stdout, err := cmd.Output()
        if err != nil {
     	        return err
        }

        // Use this instead of cmd.Stdout seems to solve the problem
        outfile.Write(stdout)
             
        return nil
}

func main() {
        args := []string{"Line 1", "Line 2", "Line 3"}
        if err := anyWrite(args, "./outfile.txt"); err != nil {
                panic(err) 
        }
} 

As per the comments in os/exec

> Stdout and Stderr specify the process's standard output and error. If
> either is nil, Run connects the corresponding file descriptor to the
> null device (os.DevNull).

huangapple
  • 本文由 发表于 2016年1月29日 13:50:06
  • 转载请务必保留本文链接:https://go.coder-hub.com/35077757.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定