-
Notifications
You must be signed in to change notification settings - Fork 0
/
golf.go
614 lines (523 loc) · 16.1 KB
/
golf.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
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
/*
Command golf provides some Go one-liner fun.
Invoke it with a snippet of Go code in the -e flag, which will be compiled
and run for you.
Additional flags such as -n turn on awk/perl-like line mode, which are useful
in processing text data. See the examples and flags sections below.
Some variables and functions are provided in the prelude package. These are
inlined and made available to the one-liner. They are common elements of
one-liner coding, for example the current line being processed in line mode.
Examples
Try these on your command line.
# Put your oneliner in -e. Here we use the builtin println.
golf -e 'println(9*6)'
# Explicitly import additional packages (don't have to be from the stdlib)
# with the -M flag, which may be repeated.
golf -M fmt -M math -e 'fmt.Println(math.Pi)'
# Some standard library packages are imported automatically.
# goimports can run for you with -g, so -M is often not needed.
golf -e 'fmt.Fprint(os.Stderr, "hi\n")'
golf -gle 'Print("The time is ", time.Now())'
# cat -n (see more about "line mode" below)
golf -n -e 'fmt.Printf("%6d %s", LineNum, Line)' MYFILE
# Use prelude Die function (takes raw error or fmtstring+args)
golf -l -e 'if data, err := os.ReadFile("MYFILE"); err != nil { Die(err) }; Print(len(data))'
# head MYFILE
golf -p -e 'if LineNum == 10 {break File}' MYFILE
# -a mode (which implies -n) automatically splits input fields.
# These can be accessed from the Fields slice, or using
# the convenient Field accessor (supports 1-based and negative indexes).
ps aux | golf -ale 'Print(Field(5))'
# Prints "and". Could also say "Field(-2)".
echo "tom, dick, and harry" | golf -ape 'Line = Field(3)'
# Input field separation uses strings.Fields by default.
# Supply the -F flag to override (-F implicitly means -a and -n).
# Can also be a regexp; see docs for prelude.GSplit.
# All users on the system.
golf -F : -e 'Print(Field(1))' /etc/passwd
# Convert TSV to CSV.
golf -F '/\t/' -ple 'for i, v := range Fields { Fields[i] = strconv.Quote(v) }; Line = Join(Fields, ",")'
# sum sizes. Note -b and E replace awk/perl BEGIN and END blocks.
ls -l | golf -alb 'sum := 0' -e 'sum += GAtoi(Field(5))' -E 'Print(sum)'
Flags
golf mimics perl's flags, but not perfectly so.
You can cluster one-letter flags, so -lane means the same as
-l -a -n -e as it does in perl.
The -b and -E flags act as replacements for awk and Perl's BEGIN and END blocks.
They are inserted before and after the -e snippet and only run once each. They
are inserted in the same scope as the -e script, so variables declared in BEGIN
are available for later blocks. -BEGIN and -END are aliases for -b and -E
respectively.
Line mode
-n puts golf in line mode: each command-line argument is treated as a filename,
which is opened in succession. Its name will populate the Filename variable.
Lines are then scanned, populating the Line variable. Stdin is read instead of
a named file if no filenames were provided.
These do the same thing as the cat example above:
cat MYFILE | golf -n -e 'fmt.Printf("%6d %s", LineNum, Line)'
golf -n -e 'fmt.Printf("%6d %s", LineNum, Line)' < MYFILE
# Unix cat concatenates multiple files. This does, too.
golf -ne 'Print(Line)' FILE1 FILE2 FILE2
The File and Line labels can be continued/broken from to skip inputs.
-p implies -n and adds a "Print(Line)" call after each line. So you can
even say:
golf -pe '' FILE1 FILE2 FILE3
In-place mode
-i causes edits to happen in-place: each input file is opened, unlinked, and
the default output (Print, Printf) is sent to a new file with the original's
name.
-I does the same, but keeps a backup of the original, according to the same
renaming rules that perl -i uses:
- if the replacement contains no "*", it is used as a literal suffix.
- otherwise, each occurrence of * is replaced with the original filename.
Like perl, we do not support crossing filesystem boundaries in backups, nor
do we create directories.
Unlike perl, in-place backup uses the -I flag, not the -i flag with an argument.
Go's standard flag library does not support optional flags. So these don't act
the same:
perl -ib FILE1 FILE2 # Runs the perl program in FILE1 with backup to FILE2.
golf -ib WORD FILE # Runs WORD in BEGIN stage, FILE will end up truncated.
No script mode
golf does not support a script mode (e.g., "golf FILE", or files with #!golf).
If you are writing a Go program in an editor, just go run it. If looking for
convenience, see if package Prelude contains anything useful.
*/
package main
import (
"bytes"
"flag"
"fmt"
"go/format"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"text/template"
"github.com/gaal/golf/prelude"
)
var (
rawSrc = stringList("e", nil, "one-liner code")
beginSrc = stringList("b", nil, "code block(s) to insert before record processing")
endSrc = stringList("E", nil, "code block(s) to insert after record processing")
flgN = flag.Bool("n", false, "line mode")
flgL = flag.Bool("l", false, "automate line-end processing. Trims input newline and adds it back on -p")
flgP = flag.Bool("p", false, "pipe mode. Implies -n and prints Line after each iteration")
flgG = flag.Bool("g", false, "run goimports")
flgA = flag.Bool("a", false, "autosplit Line to Fields. Implies -n")
flgF = flag.String("F", " ", "field separator. Implies -a and -n. See docs for GSplit")
inplace = flag.Bool("i", false, "in-place edit mode. See package doc for in-place edit")
inplaceBak = flag.String("I", "", "in-place edit mode, with backup. See package doc for in-place edit")
flgKeep = flag.Bool("k", false, "keep tempdir, for debugging")
warnings = flag.Bool("w", false, "print warnings on access to undefined fields and so on")
goVer = flag.String("goVer", "1.17", "go version to declare in go.mod file")
help = flag.Bool("h", false, "print usage help and exit")
modules = stringList("M", nil, "modules to import. May be repeated")
longFlags = map[string]bool{}
shortBoolFlags = map[string]bool{}
)
func init() {
// Some flags get long aliases.
flag.BoolVar(help, "help", false, "print usage help and exit")
flag.Var(beginSrc, "BEGIN", "code block(s) to insert before record processing")
flag.Var(endSrc, "END", "code block(s) to insert after record processing")
// Study declared flags so we can decluster e.g. -lane later.
flag.CommandLine.VisitAll(func(f *flag.Flag) {
if len(f.Name) > 1 {
longFlags[f.Name] = true
} else {
if fv, ok := f.Value.(interface{ IsBoolFlag() bool }); ok && fv.IsBoolFlag() {
shortBoolFlags[f.Name] = true
}
}
})
}
type stringListValue []string
func (v *stringListValue) Set(s string) error {
*v = append(*v, s)
return nil
}
func (v *stringListValue) String() string {
return fmt.Sprintf("%q", []string(*v))
}
// stringList returns a string slice bound to a flag.
// Repeated appearances of the flag on the command-line append to the slice.
func stringList(name string, value stringListValue, usage string) *stringListValue {
p := new(stringListValue)
flag.Var(p, name, usage)
return p
}
var errGolf = fmt.Errorf("golf returned nonzero status")
// prog collects the parameters of our one-liner program.
type prog struct {
RawArgs []string
BeginSrc []string
RawSrc []string
EndSrc []string
Src string
Imports []string
FlgN bool
FlgP bool
FlgL bool
FlgA bool
FlgF string
InPlace bool
InPlaceBak string
Warnings bool
Goimports bool
Keep bool
Prelude []byte
}
var program = template.Must(template.New("program").Parse(`// Program golfing is a one-liner wrapped by golf.
package main
import (
{{- range .Imports}}
"{{.}}"
{{- end}}
)
{{ printf "%s" .Prelude}}
func init() {
IFS = {{ printf "%q" .FlgF }}
Warnings = {{ .Warnings }}
GolfFlgL = {{ .FlgL }}
GolfInPlace = {{ .InPlace }}
GolfInPlaceBak = {{ printf "%q" .InPlaceBak }}
}
func main() {
// User -BEGIN start
{{- range .BeginSrc}}
{{.}}
{{- end }}
// User -BEGIN end
{{- if .FlgN}}
const _golfP = {{.FlgP}}
var _golfPDirty = false
_golfFlushP := func() {
if _golfPDirty {
Print(Line)
_golfPDirty = false
}
}
_golfCloseOut := func() {
if CurOut == os.Stdout {
return
}
if err := CurOut.Close(); err != nil {
Warn("golf: can't close current output: %v", err)
}
CurOut = os.Stdout
}
_golfFilenames := os.Args[1:]
if len(_golfFilenames)==0 {
_golfFilenames=[]string{"/dev/stdin"}
GolfInPlace = false
GolfInPlaceBak = ""
}
File:
for _, Filename = range _golfFilenames {
_golfFlushP()
_golfCloseOut()
_golfFile, err := os.Open(Filename)
if err != nil {
Die(err)
}
// NOTE: assumes POSIX fs semantics: a file can be renamed or deleted
// after being opened. This will probably fail on Windows.
if GolfInPlace {
if GolfInPlaceBak == "" {
// In the no-backup case, we still need to unlink the input
// before os.Create, because otherwise the input will be
// truncated before we read it.
if err := os.Remove(Filename); err !=nil {
Die("golf: can't remove input file: %v", err)
}
} else {
bakname := BackupName(Filename, GolfInPlaceBak)
if os.Rename(Filename, bakname); err != nil {
Die("golf: in-place backup: %v", err)
}
}
if CurOut, err = os.Create(Filename); err != nil {
Die("golf: can't create output: %v", err)
}
}
LineNum = 0
_golfScanner := bufio.NewScanner(_golfFile)
Line:
for _golfScanner.Scan() {
_golfFlushP()
LineNum++ // 1-based. Be compatible with awk, perl's default.
// Scanned line.
// BUG: restores newlines crudely in non-line mode.
// Should have \r when they were present in input, and should not
// insert a trailing newline on the last line if it was absent.
Line = _golfScanner.Text() {{- if not .FlgL}} + "\n"{{end}}
_golfPDirty = {{ .FlgP }}
{{if .FlgA}}
Fields = GSplit(IFS, Line)
{{- end}}
{{- end}}
// User -e start
{{- range .RawSrc}}
{{.}}
{{- end}}
// User -e end
{{- if .FlgN}}
continue Line
}
if err := _golfScanner.Err(); err != nil {
Die("%s: %v", Filename, err)
}
continue File
}
_golfFlushP()
_golfCloseOut()
{{- end}}
// User -END start
{{- range .EndSrc}}
{{.}}
{{- end }}
// User -END end
}
`))
func (p *prog) transform() error {
s := &bytes.Buffer{}
if err := program.Execute(s, p); err != nil {
return err
}
// Try to pretty it up, but stay silent about errors. The real compiler
// will give a better error message later.
if src, err := format.Source(s.Bytes()); err != nil {
p.Src = s.String()
} else {
p.Src = string(src)
}
return nil
}
// do runs the command with stdio connected.
func do(c string, args []string) error {
cmd := exec.Command(c, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return err
}
if cmd.ProcessState.ExitCode() != 0 {
return errGolf
}
return nil
}
// doQ runs the command, but elides the output if it was successful.
func doQ(c string, args []string) error {
cmd := exec.Command(c, args...)
out, err := cmd.CombinedOutput()
if err != nil {
return err
}
if cmd.ProcessState.ExitCode() != 0 {
return fmt.Errorf("%s", string(out))
}
return nil
}
func (p *prog) writeGolf(tmpdir string) bool {
tmpfile := filepath.Join(tmpdir, "golfing.go")
if err := os.WriteFile(tmpfile, []byte(p.Src), 0640); err != nil {
prelude.Warn("golf: %v", err)
return false
}
if p.Goimports {
if err := doQ("goimports", []string{"-w", "."}); err != nil {
prelude.Warn("golf: goimports: %v\n", err)
return false
}
}
needTidy := false
for _, v := range p.Imports {
if strings.Contains(v, "/") {
needTidy = true
break
}
}
if needTidy {
if err := doQ("go", []string{"mod", "init", "example.com/golf"}); err != nil {
prelude.Warn("golf: mod init: %v\n", err)
return false
}
if err := doQ("go", []string{"mod", "tidy"}); err != nil {
prelude.Warn("golf: mod tidy: %v\n", err)
return false
}
} else {
// Write it ourselves, which is faster.
tidy := fmt.Sprintf("module example.com/golf\n\ngo %s\n", *goVer)
if err := os.WriteFile("go.mod", []byte(tidy), 0640); err != nil {
prelude.Warn("golf: writing mod file: %v\n", err)
return false
}
}
return true
}
func (p *prog) run() int {
tmpdir, err := os.MkdirTemp("", "golf-")
if err != nil {
prelude.Warn("golf: mkdir tmp: %v\n", err)
return 1
}
if tmpdir, err = filepath.Abs(tmpdir); err != nil { // note =, not :=
prelude.Warn("golf: abs tmp: %v\n", err)
return 1
}
if p.Keep {
prelude.Warn(tmpdir)
} else {
defer func() {
if err := os.RemoveAll(tmpdir); err != nil {
prelude.Warn("golf: rmall tmp: %v\n", err)
// but don't fail the golf.
}
}()
}
origdir, err := os.Getwd()
if err != nil {
prelude.Warn("golf: original dir: %v\n", err)
return 1
}
if err := os.Chdir(tmpdir); err != nil {
prelude.Warn("golf: %v", err)
return 1
}
if ok := p.writeGolf(tmpdir); !ok {
return 1
}
/* y u no faster?
if err := do("go", []string{"tool", "compile", "golfing.go"}); err != nil {
prelude.Warn("compile: %v", err)
return 1
}
if err := do("go", []string{"tool", "link", "golfing.o"}); err != nil {
prelude.Warn("link: %v", err)
return 1
}
if err := do("./a.out", p.RawArgs); err != nil {
prelude.Warn("run: %v", err)
return 1
}
*/
const binname = "golfing" // should this add .exe on win32?
if err := do("go", []string{"build", "-o", binname, "."}); err != nil {
if err != errGolf {
prelude.Warn("golf: %v", err)
}
return 1
}
if err := os.Chdir(origdir); err != nil {
prelude.Warn("golf: returning to original dir: %v", err)
return 1
}
if err := do(filepath.Join(tmpdir, binname), p.RawArgs); err != nil {
if err != errGolf {
prelude.Warn("golf: %v", err)
}
return 1
}
return 0
}
func decluster() {
res := []string{os.Args[0]}
for i, v := range os.Args[1:] {
if v[0] != '-' || longFlags[v[1:]] {
// Skip a non-flag arguments and known long flags.
res = append(res, v)
continue
}
if v == "--" {
res = append(res, os.Args[i:]...)
break
}
for i, vv := range strings.Split(v[1:], "") {
if i < (len(v)-2) && !shortBoolFlags[vv] {
// This doesn't protect against -ib, unfortunately.
// (Our version of -i does not take an arg.)
prelude.Warn("-%s cannot be used inside a flag cluster", vv)
flag.PrintDefaults()
os.Exit(1)
}
res = append(res, "-"+vv)
}
}
os.Args = res
}
func dedupe(s []string) []string {
sort.Strings(s)
w := 0
for r, cur := range s {
if r > 0 && cur == s[r-1] {
continue
}
s[w] = cur
w++
}
return s[:w]
}
const helpString = `Command golf provides some Go one-liner fun.
Invoke it with a snippet of Go code in the -e flag, which will be compiled
and run for you.
golf -e 'for _, v := range []string{"hi", "bye"} { fmt.Println(v) }'
Additional flags such as -n turn on awk/perl-like line mode, which are useful
in processing text data. See the examples and more in the Godoc for
github.com/gaal/golf.
`
func main() {
// The standard Go flag package does not support flag clustering.
// This is too convenient to give up when golfing, so handle it ourselves.
decluster()
flag.Parse()
if *help {
// TODO: intentional -h output belongs on stdout.
prelude.Warn(helpString)
flag.PrintDefaults()
os.Exit(0)
}
// -F implies -a (which in turn implies -n...)
flag.Visit(func(f *flag.Flag) {
if f.Name == "F" {
*flgA = true
}
})
// Both -a and -p imply -n.
*flgN = *flgN || *flgP || *flgA
// -I implies -i.
*inplace = *inplace || len(*inplaceBak) > 0
imps := []string{"io", "os", "regexp", "strconv", "strings", "fmt"}
if *flgN {
imps = append(imps, "bufio")
}
if len(*modules) > 0 {
imps = append(imps, *modules...)
}
imps = dedupe(imps)
p := &prog{
BeginSrc: *beginSrc,
RawSrc: *rawSrc,
EndSrc: *endSrc,
RawArgs: flag.Args(),
Imports: imps,
FlgN: *flgN,
FlgP: *flgP,
FlgL: *flgL,
FlgA: *flgA,
FlgF: *flgF,
InPlace: *inplace,
InPlaceBak: *inplaceBak,
Warnings: *warnings,
Goimports: *flgG,
Keep: *flgKeep,
Prelude: prelude.Source(),
}
if err := p.transform(); err != nil {
prelude.Warn("golf: %v", err)
os.Exit(1)
}
os.Exit(p.run())
}