-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.go
167 lines (139 loc) · 3.52 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
// Command pipescratch manages a scratch file as standard input/output for a
// external command.
//
// pipescratch runs a command and creates a temporary scratch file. Each time
// the file is updated, its contents are passed to the command's standard input.
// Each time the command writes to its standard output or error, the file is
// appended a section at the end with the output as line comments.
package main
import (
"bufio"
"bytes"
"context"
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"github.com/fsnotify/fsnotify"
)
var (
editorFlag = flag.String("editor", "", "`command` to be invoked with the scratch file location as arg (empty just prints it)")
extFlag = flag.String("ext", "sql", "`extension` for scratch file")
linePrefix = flag.String("line-prefix", "-- ", "prefix for each output line")
)
func main() {
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s:\n", os.Args[0])
flag.PrintDefaults()
fmt.Fprintf(flag.CommandLine.Output(), " [command] [arg1] [arg2] ...\n")
fmt.Fprintf(flag.CommandLine.Output(), " command to manage input/output as a scratch file\n")
}
flag.Parse()
if len(flag.Args()) == 0 {
fmt.Fprintln(flag.CommandLine.Output(), "Missing non-flag arguments: [command] [arg1] [arg2] ...")
flag.Usage()
os.Exit(2)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
f, err := ioutil.TempFile("", "pipescratch-*."+(*extFlag))
try(err)
defer f.Close()
defer os.Remove(f.Name())
if *editorFlag != "" {
go exec.CommandContext(ctx, *editorFlag, f.Name()).CombinedOutput()
} else {
fmt.Println(f.Name())
}
watcher, err := fsnotify.NewWatcher()
try(err)
try(watcher.Add(f.Name()))
cmd := exec.CommandContext(ctx, flag.Args()[0], flag.Args()[1:]...)
cmdIn, err := cmd.StdinPipe()
try(err)
cmdOut, err := cmd.StdoutPipe()
try(err)
cmdErr, err := cmd.StderrPipe()
try(err)
try(cmd.Start())
outLines := make(chan string)
go readLines(outLines, cmdOut)
errLines := make(chan string)
go readLines(errLines, cmdErr)
var currOut, currErr string
var prevContents []byte
for {
select {
case ev := <-watcher.Events:
if ev.Op&fsnotify.Write != fsnotify.Write {
continue
}
_, err := f.Seek(0, 0)
try(err)
contents, err := ioutil.ReadAll(f)
try(err)
if bytes.Equal(prevContents, contents) {
continue
}
prevContents = contents
_, err = cmdIn.Write(contents)
try(err)
currOut, currErr = "", ""
continue
case err := <-watcher.Errors:
panic(err)
case line, ok := <-outLines:
if !ok {
outLines = nil
continue
}
currOut += *linePrefix + line + "\n"
case line, ok := <-errLines:
if !ok {
errLines = nil
continue
}
currErr += *linePrefix + line + "\n"
}
_, err := f.Seek(0, 0)
try(err)
oldContents := bufio.NewReader(f)
var newContents bytes.Buffer
for {
line, err := oldContents.ReadString('\n')
if line == (*linePrefix)+"~~ scratch ~~\n" {
break
}
newContents.WriteString(line)
if err != nil {
newContents.WriteString("\n")
break
}
}
fmt.Fprintf(&newContents, (*linePrefix)+"~~ scratch ~~\n%s%s", currOut, currErr)
_, err = f.Seek(0, 0)
try(err)
try(f.Truncate(0))
prevContents = newContents.Bytes()
_, err = f.Write(prevContents)
try(err)
}
}
func readLines(dst chan<- string, src io.Reader) {
buf := bufio.NewReader(src)
for {
line, err := buf.ReadString('\n')
if err != nil {
close(dst)
return
}
dst <- line[:len(line)-1]
}
}
func try(err error) {
if err != nil {
panic(err)
}
}