Skip to content

Commit

Permalink
Merge pull request #3 from mdm-code/support-stdin-stderr
Browse files Browse the repository at this point in the history
Add support to STDIN & STDERR of wrapped command
  • Loading branch information
mdm-code authored Nov 28, 2023
2 parents 8a9b176 + cab1397 commit 9a284bd
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 47 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ characters. The output is going to be written to `stdout` accordingly. This
lets you use `black` in `vim` as if it was a regular filter command, which
makes life much easier for a regular Python dev.

In case the wrapped command writes the formatted code output to `stdout` or
`stderr`, `duct` has two flags `-stdout` and `-stderr` that attach them to the
wrapped command instead of redirecting the whole R/W flow through a temporary
file.


## Development

Expand Down
13 changes: 9 additions & 4 deletions cmd/duct/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ Duct wraps a code formatter inside of a stdin to stdout filter-like data flow.
Usage:
duct [args...]
duct [OPTIONS] [args...]
Options:
-h, --help show this help message and exit
-h, -help, --help show this help message and exit
-stdout, --stdout attach stdout of the wrapped command
-stderr, --stderr attach stderr of the wrapped command
Example:
Expand Down Expand Up @@ -47,8 +49,11 @@ Output:
q = Queue()
print_size(q)
The program wraps a code formatter, which accepts file names as commands
The program wraps a code formatter, which accepts file names as command
arguments instead of reading from standard input data stream, inside of a
standard Unix stdin to stdout filter-like data flow.
standard Unix stdin to stdout filter-like data flow. The -stdout and -stderr
flags replace stdout and stderr of duct with stdout and stderr of the wrapped
command. It's useful when the wrapped command reads code from files but writes
its output to stdout and/or stderr instead of writing directly to files.
*/
package main
91 changes: 60 additions & 31 deletions cmd/duct/main.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
package main

import (
"flag"
"fmt"
"io"
"os"

"github.com/mdm-code/duct"
)

var usage = `duct - add stdin and stdout to a code formatter
const (
exitSuccess int = iota
exitFailure
)

var (
usage = `duct - add stdin and stdout to a code formatter
Duct wraps a code formatter inside of a stdin to stdout filter-like data flow.
Usage:
duct [args...]
duct [OPTIONS] [args...]
Options:
-h, --help show this help message and exit
-h, -help, --help show this help message and exit
-stdout, --stdout attach stdout of the wrapped command
-stderr, --stderr attach stderr of the wrapped command
Example:
duct black -l 79 <<EOF
Expand Down Expand Up @@ -51,47 +61,66 @@ Output:
q = Queue()
print_size(q)
The program wraps a code formatter, which accepts file names as commands
The program wraps a code formatter, which accepts file names as command
arguments instead of reading from standard input data stream, inside of a
standard Unix stdin to stdout filter-like data flow.
standard Unix stdin to stdout filter-like data flow. The -stdout and -stderr
flags replace stdout and stderr of duct with stdout and stderr of the wrapped
command. It's useful when the wrapped command reads code from files but writes
its output to stdout and/or stderr instead of writing directly to files.
`

attachStdout bool
attachStderr bool

cmdStdout io.Writer = duct.Discard
cmdStderr io.Writer = duct.Discard
)

func main() {
os.Exit(run())
}

func run() int {
if len(os.Args) == 1 || (len(os.Args) > 1 && isHelp(os.Args[1])) {
fmt.Fprintf(os.Stdout, usage)
return 0
flag.Usage = func() {
w := flag.CommandLine.Output()
fmt.Fprint(w, usage)
}
flag.BoolVar(&attachStdout, "stdout", false, "")
flag.BoolVar(&attachStderr, "stderr", false, "")
flag.Parse()

nonFlagArgs := flag.Args()
if len(nonFlagArgs) < 1 {
fmt.Fprintf(os.Stderr, "ERROR: missing command to wrap")
return exitFailure
}
f, err := os.CreateTemp("", duct.Pattern)

tempFile, err := os.CreateTemp("", duct.Pattern)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to create a temporary file: %s", err)
return 1
fmt.Fprintf(os.Stderr, "ERROR: failed to create a temporary file: %s", err)
return exitFailure
}
fds, closer := duct.NewFDs(os.Stdin, os.Stdout, duct.Discard, f)
defer os.Remove(f.Name())
ductFDs, closer := duct.NewFDs(os.Stdin, os.Stdout, duct.Discard, tempFile)
defer os.Remove(tempFile.Name())
defer closer()
args := []string{}
if len(os.Args) > 1 {
args = append(args, os.Args[2:]...)

cmdName, cmdArgs := nonFlagArgs[0], nonFlagArgs[1:]
cmdArgs = append(cmdArgs, tempFile.Name())
if attachStdout {
cmdStdout = os.Stdout
}
args = append(args, f.Name())
cmd := duct.Cmd(os.Args[1], duct.Discard, duct.Discard, args...)
err = duct.Wrap(cmd, fds)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to reformat the file: %s", err)
return 1
if attachStderr {
cmdStderr = os.Stderr
}
return 0
}

func isHelp(arg string) bool {
for _, flag := range []string{"-h", "-help", "--help"} {
if arg == flag {
return true
}
cmd := duct.Cmd(cmdName, cmdStdout, cmdStderr, cmdArgs...)
if attachStdout || attachStderr {
err = duct.WrapWriteOnly(cmd, ductFDs)
} else {
err = duct.Wrap(cmd, ductFDs)
}
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: failed to reformat the file: %s", err)
return exitFailure
}
return false
return exitSuccess
}
5 changes: 5 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,10 @@ intermediate temporary file. The name of the file gets passed to as one of the
positional arguments of the named program to be executed. The modified contents
of the file are then re-read and written out the standard output. This way the
wrapped program can be used as a regular Unix filter.
Some code formatters take file names but do not modify files directly. Instead
they write the formatted code to stdout or stderr. This scenario is supported
by a the WrapWrite function that writes code from stdin to the temporary file
but relies on the command's own stdout and/or stderr to write out the output.
*/
package duct
21 changes: 20 additions & 1 deletion duct.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const Pattern = `duct-*`

// Discard is a WriteCloser that does nothing when either Write or Close
// methods are invoked. Ever call succeeds.
var Discard io.WriteCloser = discard{}
var Discard discard

// NilFDError indicates that a file descriptor for read/write operation is nil.
var NilFDError error = errors.New("nil file descriptor")
Expand Down Expand Up @@ -106,3 +106,22 @@ func Wrap(cmd Runner, fds *FDs) error {
}
return nil
}

// WrapWriteOnly executes the provided named formatter program wrapping the
// temporary file write operation.
//
// Code to be formatted is read from the fds.Stdin and written to fds.TempFile
// to allow the wrapped command to read code from the temporary file and handle
// its output using the command's own stdout and/or stderr.
func WrapWriteOnly(cmd Runner, fds *FDs) error {
in := bufio.NewReader(fds.Stdin)
_, err := in.WriteTo(fds.TempFile)
if err != nil && !errors.Is(err, io.EOF) {
return err
}
err = cmd.Run()
if err != nil {
return err
}
return nil
}
44 changes: 33 additions & 11 deletions duct_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,18 +140,34 @@ func TestWrapSuccess(t *testing.T) {
}
}

// Verify if WrapWriteOnly() gets called successfully.
func TestWrapWriteOnlySuccess(t *testing.T) {
cmd := MockedCmd{}
fds, _ := NewFDs(
MockedReadCloser{},
MockedWriteCloser{},
MockedWriteCloser{},
MockedReadWriteSeekCloser{},
)
err := WrapWriteOnly(cmd, fds)
if err != nil {
t.Error("WrapWrapOnly() should pass without any errors in this scenario")
}
}

// Test the Cmd() constructor interface agreement.
func TestCmdConstructor(t *testing.T) {
var _ Runner
_ = Cmd("black", MockedWriter{}, MockedWriter{}, []string{"main.py"}...)
}

// Check if Wrap() fails in the predefined scenarios.
func TestWrapRunFailed(t *testing.T) {
// Check if calls to Wrap and WrapWriteOnly fail in the predefined scenarios.
func TestWrapersRunFailed(t *testing.T) {
cases := []struct {
name string
cmd Runner
fds *FDs
name string
cmd Runner
fds *FDs
funcs []func(Runner, *FDs) error
}{
{
"runner-fail",
Expand All @@ -162,6 +178,7 @@ func TestWrapRunFailed(t *testing.T) {
MockedWriteCloser{},
MockedReadWriteSeekCloser{},
},
[]func(Runner, *FDs) error{Wrap, WrapWriteOnly},
},
{
"stdout-fail",
Expand All @@ -172,6 +189,7 @@ func TestWrapRunFailed(t *testing.T) {
MockedWriteCloser{},
MockedReadWriteSeekCloser{},
},
[]func(Runner, *FDs) error{Wrap},
},
{
"tmpfile-write-fail",
Expand All @@ -182,6 +200,7 @@ func TestWrapRunFailed(t *testing.T) {
MockedWriteCloser{},
MockedReadWriteSeekCloserFail2{},
},
[]func(Runner, *FDs) error{Wrap, WrapWriteOnly},
},
{
"tmpfile-read-fail",
Expand All @@ -192,14 +211,17 @@ func TestWrapRunFailed(t *testing.T) {
MockedWriteCloser{},
MockedReadWriteSeekCloserFail{},
},
[]func(Runner, *FDs) error{Wrap},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
err := Wrap(c.cmd, c.fds)
if err == nil {
t.Error("the following Wrap attributes should make it fail")
}
})
for _, fn := range c.funcs {
t.Run(c.name, func(t *testing.T) {
err := fn(c.cmd, c.fds)
if err == nil {
t.Error("the following Wrap attributes should make it fail")
}
})
}
}
}

0 comments on commit 9a284bd

Please sign in to comment.