How to wrap exec.Command inside an io.Writer

278 views Asked by At

I'm trying to compress a JPEG image in go using mozjpeg. Since it doesn't have official go binding, I think I'll just invoke its CLI to do the compression.

I try to model the usage after compress/gzip:

c := jpeg.NewCompresser(destFile)
_, err := io.Copy(c, srcFile)

Now the question is, how do I wrap the CLI inside Compresser so it can support this usage?

I tried something like this:

type Compresser struct {
    cmd exec.Command
}

func NewCompressor(w io.Writer) *Compresser {
    cmd := exec.Command("jpegtran", "-copy", "none")
    cmd.Stdout = w
    c := &Compresser{cmd}
    return c
}

func (c *Compresser) Write(p []byte) (n int, err error) {
    if c.cmd.Process == nil {
        err = c.cmd.Start()
        if err != nil {
            return
        }
    }
    // How do I write p into c.cmd.Stdin?
}

But couldn't finish it.

Also, a second question is, when do I shut down the command? How to shut down the command?

2

There are 2 answers

0
Kiril On

You should take a look at the Cmd.StdinPipe. There is an example in the documentation, which suits your case:

package main

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

func main() {
    cmd := exec.Command("cat")
    stdin, err := cmd.StdinPipe()
    if err != nil {
        log.Fatal(err)
    }

    go func() {
        defer stdin.Close()
        io.WriteString(stdin, "values written to stdin are passed to cmd's standard input")
    }()

    out, err := cmd.CombinedOutput()
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("%s\n", out)
}

In this case, CombinedOutput() executes your command, and the execution is finished, when there are no more bytes to read from out.

0
Martin Campbell On

As per Kiril's answer, use the cmd.StdInPipe to pass on the data you receive to Write.

However, in terms of closing, I'd be tempted to implement io.Closer. This would make *Compresser automatically implement the io.WriteCloser interface.

I would use Close() as the notification that there is no more data to be sent and that the command should be terminated. Any non-zero exit code returned from the command that indicates failure could be caught and returned as an error.

I would be wary of using CombinedOutput() inside Write() in case you have a slow input stream. The utility could finish processing the input stream and be waiting for more data. This would be incorrectly detected as command completion and would result in an invalid output.

Remember, the Write method can be called an indeterminate number of times during IO operations.