forked from zmwangx/ets
-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
272 lines (237 loc) · 7.82 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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
package main
import (
"bufio"
"bytes"
"fmt"
"io"
"log"
"os"
"os/exec"
"os/signal"
"regexp"
"strconv"
"syscall"
"time"
"github.com/creack/pty"
"github.com/mattn/go-runewidth"
"github.com/riywo/loginshell"
flag "github.com/spf13/pflag"
)
var version = "unknown"
// Regexp to strip ANSI escape sequences from string. Credit:
// https://github.com/chalk/ansi-regex/blob/2b56fb0c7a07108e5b54241e8faec160d393aedb/index.js#L4-L7
// https://github.com/acarl005/stripansi/blob/5a71ef0e047df0427e87a79f27009029921f1f9b/stripansi.go#L7
var ansiEscapes = regexp.MustCompile("[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))")
func printStreamWithTimestamper(r io.Reader, timestamper *Timestamper, delim string) {
scanner := bufio.NewScanner(r)
// Split on \r\n|\r|\n, and return the line as well as the line ending (\r
// or \n is preserved, \r\n is collapsed to \n). Adaptation of
// bufio.ScanLines.
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
lfpos := bytes.IndexByte(data, '\n')
crpos := bytes.IndexByte(data, '\r')
if crpos >= 0 {
if lfpos < 0 || lfpos > crpos+1 {
// We have a CR-terminated "line".
return crpos + 1, data[0 : crpos+1], nil
}
if lfpos == crpos+1 {
// We have a CRLF-terminated line.
return lfpos + 1, append(data[0:crpos], '\n'), nil
}
}
if lfpos >= 0 {
// We have a LF-terminated line.
return lfpos + 1, data[0 : lfpos+1], nil
}
// If we're at EOF, we have a final, non-terminated line. Return it.
if atEOF {
return len(data), data, nil
}
// Request more data.
return 0, nil, nil
})
for scanner.Scan() {
fmt.Print(timestamper.CurrentTimestampString(), delim, scanner.Text())
}
}
func runCommandWithTimestamper(args []string, timestamper *Timestamper, delim string) error {
// Calculate optimal pty size, taking into account horizontal space taken up by timestamps.
getPtyWinsize := func() *pty.Winsize {
winsize, err := pty.GetsizeFull(os.Stdin)
if err != nil {
// Most likely stdin isn't a tty, in which case we don't care.
return winsize
}
totalCols := winsize.Cols
plainTimestampString := ansiEscapes.ReplaceAllString(timestamper.CurrentTimestampString(), "")
// Timestamp width along with one space character.
occupiedWidth := uint16(runewidth.StringWidth(plainTimestampString)) + 1
var effectiveCols uint16 = 0
if occupiedWidth < totalCols {
effectiveCols = totalCols - occupiedWidth
}
winsize.Cols = effectiveCols
// Best effort estimate of the effective width in pixels.
if totalCols > 0 {
winsize.X = winsize.X * effectiveCols / totalCols
}
return winsize
}
command := exec.Command(args[0], args[1:]...)
ptmx, err := pty.StartWithSize(command, getPtyWinsize())
if err != nil {
return err
}
defer func() { _ = ptmx.Close() }()
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGWINCH, syscall.SIGINT, syscall.SIGTERM)
go func() {
for sig := range sigs {
switch sig {
case syscall.SIGWINCH:
if err := pty.Setsize(ptmx, getPtyWinsize()); err != nil {
log.Println("error resizing pty:", err)
}
case syscall.SIGINT:
_ = syscall.Kill(-command.Process.Pid, syscall.SIGINT)
case syscall.SIGTERM:
_ = syscall.Kill(-command.Process.Pid, syscall.SIGTERM)
default:
}
}
}()
sigs <- syscall.SIGWINCH
go func() { _, _ = io.Copy(ptmx, os.Stdin) }()
printStreamWithTimestamper(ptmx, timestamper, delim)
return command.Wait()
}
func main() {
log.SetFlags(log.Flags() &^ (log.Ldate | log.Ltime))
var elapsedMode = flag.BoolP("elapsed", "s", false, "show elapsed timestamps")
var incrementalMode = flag.BoolP("incremental", "i", false, "show incremental timestamps")
var format = flag.StringP("format", "f", "", "show timestamps in this format")
var delim = flag.StringP("delim", "d", " ", "delimiter after timestamp")
var utc = flag.BoolP("utc", "u", false, "show absolute timestamps in UTC")
var timezoneName = flag.StringP("timezone", "z", "", "show absolute timestamps in this timezone, e.g. America/New_York")
var color = flag.BoolP("color", "c", false, "show timestamps in color")
var printHelp = flag.BoolP("help", "h", false, "print help and exit")
var printVersion = flag.BoolP("version", "v", false, "print version and exit")
flag.CommandLine.SortFlags = false
flag.SetInterspersed(false)
flag.Usage = func() {
fmt.Fprintf(os.Stderr, `
ets -- command output timestamper
ets prefixes each line of a command's output with a timestamp. Lines are
delimited by CR, LF, or CRLF.
Usage:
%s [-s | -i] [-f format] [-d delim] [-u | -z timezone] command [arg ...]
%s [options] shell_command
%s [options]
The three usage strings correspond to three command execution modes:
* If given a single command without whitespace(s), or a command and its
arguments, execute the command with exec in a pty;
* If given a single command with whitespace(s), the command is treated as
a shell command and executed as SHELL -c shell_command, where SHELL is
the current user's login shell, or sh if login shell cannot be determined;
* If given no command, output is read from stdin, and the user is
responsible for piping in a command's output.
There are three mutually exclusive timestamp modes:
* The default is absolute time mode, where timestamps from the wall clock
are shown;
* -s, --elapsed turns on elapsed time mode, where every timestamp is the
time elapsed from the start of the command (using a monotonic clock);
* -i, --incremental turns on incremental time mode, where every timestamp is
the time elapsed since the last timestamp (using a monotonic clock).
The default format of the prefixed timestamps depends on the timestamp mode
active. Users may supply a custom format string with the -f, --format option.
The format string is basically a strftime(3) format string; see the man page
or README for details on supported formatting directives.
The timezone for absolute timestamps can be controlled via the -u, --utc
and -z, --timezone options. --timezone accepts IANA time zone names, e.g.,
America/Los_Angeles. Local time is used by default.
Options:
`, os.Args[0], os.Args[0], os.Args[0])
flag.PrintDefaults()
}
flag.Parse()
if *printHelp {
flag.Usage()
os.Exit(0)
}
if *printVersion {
fmt.Println(version)
os.Exit(0)
}
mode := AbsoluteTimeMode
if *elapsedMode && *incrementalMode {
log.Fatal("conflicting flags --elapsed and --incremental")
}
if *elapsedMode {
mode = ElapsedTimeMode
}
if *incrementalMode {
mode = IncrementalTimeMode
}
if *format == "" {
if mode == AbsoluteTimeMode {
*format = "[%F %T]"
} else {
*format = "[%T]"
}
}
s, err := strconv.Unquote(`"` + *delim + `"`)
if err != nil {
log.Fatalf("error parsing delimiter string: %s", err)
} else {
*delim = s
}
timezone := time.Local
if *utc && *timezoneName != "" {
log.Fatal("conflicting flags --utc and --timezone")
}
if *utc {
timezone = time.UTC
}
if *timezoneName != "" {
location, err := time.LoadLocation(*timezoneName)
if err != nil {
log.Fatal(err)
}
timezone = location
}
if *color {
*format = "\x1b[32m" + *format + "\x1b[0m"
}
args := flag.Args()
timestamper, err := NewTimestamper(*format, mode, timezone)
if err != nil {
log.Fatal(err)
}
exitCode := 0
if len(args) == 0 {
printStreamWithTimestamper(os.Stdin, timestamper, *delim)
} else {
if len(args) == 1 {
arg0 := args[0]
if matched, _ := regexp.MatchString(`\s`, arg0); matched {
shell, err := loginshell.Shell()
if err != nil {
shell = "sh"
}
args = []string{shell, "-c", arg0}
}
}
if err = runCommandWithTimestamper(args, timestamper, *delim); err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
exitCode = exitErr.ExitCode()
} else {
log.Fatal(err)
}
}
}
os.Exit(exitCode)
}