-
Notifications
You must be signed in to change notification settings - Fork 1
/
util.go
310 lines (284 loc) · 6.88 KB
/
util.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
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
package main
import (
"fmt"
"io/ioutil"
"net/http"
"os"
"path"
"regexp"
"strconv"
"strings"
"syscall"
"time"
"github.com/spf13/pflag"
"golang.org/x/exp/constraints"
"golang.org/x/exp/slices"
"golang.org/x/term"
)
var ErrUnrecognizedTime error = ObserveError{Msg: "the time format is not recognized"}
var ErrInvalidDuration error = ObserveError{Msg: "the duration value is not recognized"}
var ErrSnapMustBePositive error = ObserveError{Msg: "the snap duration must be greater than 0"}
func WrapPrefix(str string, leading string, width int) string {
var ret string
last := 0
breaknow := false
for i := 0; i < len(str); i++ {
if i == width || breaknow {
if last == 0 || last < width-12 {
last = i
}
ret = ret + leading + str[:last] + "\n"
str = str[last:]
i = 0
breaknow = false
}
ch := str[i]
switch {
case ch >= 'a' && ch <= 'z':
case ch >= 'A' && ch <= 'Z':
case ch >= '0' && ch <= '9':
case ch == '_':
case ch == '\n':
breaknow = true
default:
last = i + 1
}
}
if len(str) > 0 {
ret = ret + leading + str + "\n"
}
return ret
}
// Boo hiss reads the value directly -- fixup this by making it part of Config
// and passing Config everywhere it's needed.
func GetConfigFilePath() string {
if *FlagConfigFile != "" {
return *FlagConfigFile
}
return path.Join(os.Getenv("HOME"), ".config/observe.yaml")
}
func ReadPasswordFromTerminal(prompt string) ([]byte, error) {
var pwdata []byte
var err error
if term.IsTerminal(int(syscall.Stdin)) {
os.Stderr.WriteString(prompt)
pwdata, err = term.ReadPassword(int(syscall.Stdin))
fmt.Fprintf(os.Stderr, "\n")
} else {
pwdata, err = ioutil.ReadAll(os.Stdin)
// I can't quite Trim, because someone might have a password that begins or ends with a space.
for len(pwdata) > 0 && (pwdata[len(pwdata)-1] == '\n' || pwdata[len(pwdata)-1] == '\r') {
pwdata = pwdata[:len(pwdata)-1]
}
}
return pwdata, err
}
func GetHostname() string {
hn, _ := os.Hostname() // should just work
if hn == "" {
hn = os.Getenv("HOSTNAME") // Unix fallback
}
if hn == "" {
hn = os.Getenv("COMPUTERNAME") // Windows fallback
}
if hn == "" {
hn = "unknown-host"
}
return hn
}
func CountFlags(fs *pflag.FlagSet, flags ...string) int {
n := 0
for _, f := range flags {
if fs.Lookup(f).Changed {
n++
}
}
return n
}
func LoadQueryTextFromFile(fs fileSystem, filepath string) (string, error) {
buf, err := fs.ReadFile(filepath)
return string(buf), err
}
var timePiecesRegexAlt = regexp.MustCompile(`^(now)? *([+-][0-9]*[smhd])? *(@[0-9]*[smhd])?$`)
var timePiecesRegex = regexp.MustCompile(`^([^-+@][^@]*)?(@[0-9]*[smhd])? *([+-][0-9]*[smhd])?$`)
// The given input strings should be a date-time in YYYY-MM-DD HH:MM:SS format,
// or some variation thereof. Also, epoch values of seconds, milliseconds,
// nanoseconds are allowed, as long as they are sufficiently positive to be
// disabiguated. Finally, relative times are also supported, and will be
// interpreted relative to the 'now' argument.
func ParseTime(tm string, now time.Time) (time.Time, error) {
tm = strings.TrimSpace(tm)
if tm == "" {
return now, nil
}
pieces := timePiecesRegexAlt.FindStringSubmatch(tm)
if len(pieces) == 4 && pieces[0] != "" {
// the 'now-3h@1h' form flips the delta and snap
pieces[2], pieces[3] = pieces[3], pieces[2]
} else {
pieces = timePiecesRegex.FindStringSubmatch(tm)
if len(pieces) != 4 || pieces[0] == "" {
return time.Time{}, ErrUnrecognizedTime
}
}
// time
abstime := now
var err error
if pieces[1] != "" {
abstime, err = ReadAbsoluteTime(pieces[1], now)
if err != nil {
return time.Time{}, err
}
}
// snap
if len(pieces[2]) > 0 {
delta, err := ReadDuration(pieces[2][1:])
if err != nil {
return time.Time{}, NewObserveError(err, "time snap")
}
if delta <= 0 {
return time.Time{}, ErrSnapMustBePositive
}
abstime = abstime.Truncate(delta)
}
// delta
if len(pieces[3]) > 0 {
delta, err := ReadDuration(pieces[3])
if err != nil {
return time.Time{}, NewObserveError(err, "time offset")
}
abstime = abstime.Add(delta)
}
return abstime, nil
}
func ReadAbsoluteTime(tm string, rel time.Time) (time.Time, error) {
// If I have an epoch time, from UNIX, from JavaScript, or from OPAL, I can
// punch it in here as a number.
if i64, err := strconv.ParseInt(tm, 10, 64); err == nil {
if i64 >= 1000000000 && i64 < 4999999999 { // seconds
return time.Unix(i64, 0), nil
} else if i64 >= 1000000000000 && i64 < 9999999999999 { // milliseconds
return time.Unix(i64/1000, i64%1000), nil
} else if i64 >= 1000000000000000000 { // nanoseconds
return time.Unix(0, i64), nil
}
}
// The German, Canadian, British, and US ways of writing dates
// are too ambiguous and contradictory, so we *only* support
// ISO-style YYYY-MM-DD 24-hour formats.
for _, timeformat := range []string{
time.RFC3339Nano,
time.RFC3339,
"2006-01-02 15:04:05.999999999 -07:00",
"2006-01-02 15:04:05.999 -07:00",
"2006-01-02 15:04:05 -0700",
"2006-01-02 15:04:05.999999999",
"2006-01-02 15:04:05.999",
"2006-01-02 15:04:05",
} {
if t, err := time.Parse(timeformat, tm); err == nil {
return t, nil
}
}
if tm == "now" {
return rel, nil
}
return time.Time{}, ErrUnrecognizedTime
}
func ReadDuration(tm string) (time.Duration, error) {
if tm == "" {
return 0, ErrInvalidDuration
}
tmlen := len(tm)
unit := tm[tmlen-1]
tm = tm[:tmlen-1]
var i64 int64
var err error
switch tm {
case "-":
i64 = -1
case "+", "":
i64 = 1
default:
i64, err = strconv.ParseInt(tm, 10, 64)
if err != nil {
return 0, ErrInvalidDuration
}
}
switch unit {
case 's':
return time.Duration(i64) * time.Second, nil
case 'm':
return time.Duration(i64) * time.Minute, nil
case 'h':
return time.Duration(i64) * time.Hour, nil
case 'd':
return time.Duration(i64) * 24 * time.Hour, nil
}
return 0, ErrInvalidDuration
}
func maybe[T any](t *T) any {
if t == nil {
return nil
}
return *t
}
func must[T any](t T, e error) T {
if e != nil {
panic(fmt.Errorf("unexpected error: %w", e))
}
return t
}
// note: this sorts in place
func sorted[E constraints.Ordered](s []E) []E {
slices.Sort(s)
return s
}
// http.Header.Set() doesn't return the Header object, so it's not usable inline
func headers(args ...string) http.Header {
ret := http.Header{}
for i := 0; i != len(args); i += 2 {
ret.Set(args[i], args[i+1])
}
return ret
}
func maybeIntStr(i *int64) string {
if i == nil {
return ""
}
return strconv.FormatInt(*i, 10)
}
func maybeStr(s *string) string {
if s == nil {
return ""
}
return *s
}
func maybeAnyString(s any) *string {
if s == nil {
return nil
}
ret := s.(string)
return &ret
}
func maybeAnyInt(i any) *int64 {
if i == nil {
return nil
}
ret := i.(int64)
return &ret
}
func maybeStringAny(s *string) any {
if s == nil {
return nil
}
ret := *s
return ret
}
func maybeIntAny(i *int64) any {
if i == nil {
return nil
}
ret := *i
return ret
}