├── .gitignore ├── go.mod ├── scripts └── update_readme.sh ├── .github └── workflows │ └── tests.yml ├── LICENSE.txt ├── go.sum ├── README.md ├── prig_test.go └── prig.go /.gitignore: -------------------------------------------------------------------------------- 1 | prig 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/benhoyt/prig 2 | 3 | go 1.17 4 | 5 | require golang.org/x/tools v0.1.11 6 | 7 | require ( 8 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect 9 | golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /scripts/update_readme.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | go build 6 | 7 | ./prig -h >help.txt 8 | 9 | ./prig 'if Match("^Prig v1", S(0)) { return }' \ 10 | 'Println(S(0))' \ 11 | head.txt 12 | 13 | ./prig -b 'start, end := false, false' \ 14 | 'if Match("^Prig v1", S(0)) { start = true }' \ 15 | 'if start && Match("^```", S(0)) { end = true }' \ 16 | 'if end { Println(S(0)) }' \ 17 | tail.txt 18 | 19 | cat head.txt help.txt tail.txt >README.md 20 | 21 | rm help.txt head.txt tail.txt 22 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | tests: 11 | strategy: 12 | matrix: 13 | go: ['1.17', '1.18'] 14 | os: ['ubuntu-latest', 'windows-latest', 'macos-latest'] 15 | 16 | runs-on: ${{ matrix.os }} 17 | 18 | name: Go ${{ matrix.go }} on ${{ matrix.os }} 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | 23 | - name: Set up Go 24 | uses: actions/setup-go@v2 25 | with: 26 | go-version: ${{ matrix.go }} 27 | 28 | - name: Run Tests 29 | run: | 30 | go test 31 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ben Hoyt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 2 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 3 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 4 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= 5 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 6 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 7 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 8 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 9 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 10 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 11 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 12 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 13 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 14 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 15 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 16 | golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b h1:2n253B2r0pYSmEV+UNCQoPfU/FiaizQEK5Gu4Bq4JE8= 17 | golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 18 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 19 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 20 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 21 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 22 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 23 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 24 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 25 | golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY= 26 | golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= 27 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Prig: the snobbish AWK 3 | 4 | Prig is for **P**rocessing **R**ecords **I**n **G**o. It's like AWK, but snobbish (Go! static typing!). It's also faster to execute, and if you know Go, you don't need to learn AWK. 5 | 6 | You can also read my article on [**why and how I wrote Prig**](https://benhoyt.com/writings/prig/). The tl;dr is "no good reason, I'm a geek!" :-) 7 | 8 | 9 | ## How to use Prig 10 | 11 | To install `prig`, make sure Go is [installed](https://go.dev/doc/install) and then type `go install github.com/benhoyt/prig@latest`. Prig itself runs the generated code using `go build`, so even once you have a `prig` executable it requires the Go compiler to be installed. 12 | 13 | As a simple example, you can try the following script. It prints a modified version of the second field of each line of input (the full URL in this example): 14 | 15 | ``` 16 | $ cat logs.txt 17 | GET /robots.txt HTTP/1.1 18 | HEAD /README.md HTTP/1.1 19 | GET /wp-admin/ HTTP/1.0 20 | 21 | $ prig 'Println("https://example.com" + S(2))' ", 277 | args: []string{`-F,`, `Printf("%v.%v.%v\n", S(1), S(2), S(3))`}, 278 | in: "a,b,c\n one,two ,three\nxx,yy", 279 | out: "a.b.c\n one.two .three\nxx.yy.\n", 280 | }, 281 | { 282 | name: "one-character field separator -F ", 283 | args: []string{`-F`, `,`, `Printf("%v.%v.%v\n", S(1), S(2), S(3))`}, 284 | in: "a,b,c\n one,two ,three\nxx,yy", 285 | out: "a.b.c\n one.two .three\nxx.yy.\n", 286 | }, 287 | { 288 | name: "regex field separator", 289 | args: []string{`-F[.,]`, `Printf("%v.%v.%v\n", S(1), S(2), S(3))`}, 290 | in: "a,b.c\n one.two ,three\nxx,yy", 291 | out: "a.b.c\n one.two .three\nxx.yy.\n", 292 | }, 293 | { 294 | name: "regex field separator error", 295 | args: []string{`-F[.,`, `Println()`}, 296 | err: "invalid field separator: error parsing regexp: missing closing ]: `[.,`\n", 297 | }, 298 | { 299 | name: "version -V", 300 | args: []string{`-V`}, 301 | out: version + "\n", 302 | }, 303 | { 304 | name: "version --version", 305 | args: []string{`--version`}, 306 | out: version + "\n", 307 | }, 308 | { 309 | name: "auto-import", 310 | args: []string{`-b`, `Printf("%.2f", math.Pi)`}, 311 | out: "3.14", 312 | }, 313 | { 314 | name: "import flag - text", 315 | args: []string{ 316 | `-i`, `text/template`, 317 | `-b`, `t := template.Must(template.New("").Parse("{{.}}"))`, 318 | `-b`, `err := t.Execute(os.Stdout, "foo"); if err != nil { panic(err) }`, 319 | }, 320 | out: "foo", 321 | }, 322 | { 323 | name: "import flag - html", 324 | args: []string{ 325 | `-i`, `html/template`, 326 | `-b`, `t := template.Must(template.New("").Parse("{{.}}"))`, 327 | `-b`, `err := t.Execute(os.Stdout, "foo"); if err != nil { panic(err) }`, 328 | }, 329 | out: "<b>foo</b>", 330 | }, 331 | } 332 | 333 | func TestPrig(t *testing.T) { 334 | runTests(t, prigTests) 335 | } 336 | 337 | func runTests(t *testing.T, tests []test) { 338 | t.Helper() 339 | for _, test := range tests { 340 | t.Run(test.name, func(t *testing.T) { 341 | in := strings.NewReader(test.in) 342 | args := []string{} 343 | if *goExe != "" { 344 | args = append(args, "-g", *goExe) 345 | } 346 | args = append(args, test.args...) 347 | cmd := exec.Command("./prig", args...) 348 | cmd.Stdin = in 349 | outputBytes, err := cmd.CombinedOutput() 350 | output := string(outputBytes) 351 | if err != nil { 352 | if test.err == "" { 353 | t.Fatalf("expected success, got error:\n%s", output) 354 | } 355 | if output != test.err { 356 | t.Fatalf("expected first error, got second:\n%s\n-----\n%s", test.err, output) 357 | } 358 | return 359 | } 360 | if test.err != "" { 361 | t.Fatalf("got success, expected error:\n%s", test.err) 362 | } 363 | if output != test.out { 364 | t.Fatalf("expected first output, got second:\n%s\n-----\n%s", test.out, output) 365 | } 366 | }) 367 | } 368 | } 369 | 370 | func TestExamples(t *testing.T) { 371 | tests := []test{ 372 | { 373 | name: "HelloWorld", 374 | args: exampleToArgs(t, exampleHelloWorld), 375 | out: "Hello, world! 3.141592653589793\n", 376 | }, 377 | { 378 | name: "Average", 379 | args: exampleToArgs(t, exampleAverage), 380 | in: "a b 400\nc d 200\ne f 200\ng h 200", 381 | out: "250\n", 382 | }, 383 | { 384 | name: "Milliseconds", 385 | args: exampleToArgs(t, exampleMilliseconds), 386 | in: "1 GET 3.14159\n2 HEAD 4.0\n3 GET 1.0\n4 GET 100.23\n", 387 | out: "3142ms\n4000ms\n1000ms\n100230ms\n", 388 | }, 389 | { 390 | name: "Frequencies", 391 | args: exampleToArgs(t, exampleFrequencies), 392 | in: "The foo bar foo bar\nthe the the\nend.\n", 393 | out: "the 4\nfoo 2\nbar 2\nend. 1\n", 394 | }, 395 | } 396 | runTests(t, tests) 397 | } 398 | 399 | func exampleToArgs(t *testing.T, s string) []string { 400 | t.Helper() 401 | if !strings.HasPrefix(s, "prig ") { 402 | t.Fatal(`example must start with "prig "`) 403 | } 404 | s = s[5:] 405 | s = strings.ReplaceAll(s, "\\\n", "") 406 | parts := strings.Split(s, "'") 407 | var args []string 408 | for i, part := range parts { 409 | if i%2 == 0 { 410 | args = append(args, strings.Fields(part)...) 411 | } else { 412 | args = append(args, part) 413 | } 414 | } 415 | return args 416 | } 417 | -------------------------------------------------------------------------------- /prig.go: -------------------------------------------------------------------------------- 1 | // Prig is for Processing Records In Go. It's like AWK, but snobbish. 2 | // 3 | // It is based on a similar idea for Nim: 4 | // https://github.com/c-blake/cligen/blob/master/examples/rp.nim 5 | // 6 | // Prig code is licensed under the MIT License. 7 | // 8 | // See README.md for more details. 9 | package main 10 | 11 | import ( 12 | "bytes" 13 | "fmt" 14 | "os" 15 | "os/exec" 16 | "path/filepath" 17 | "regexp" 18 | "runtime" 19 | "strconv" 20 | "strings" 21 | "text/template" 22 | "unicode/utf8" 23 | 24 | importspkg "golang.org/x/tools/imports" 25 | ) 26 | 27 | const version = "v1.1.0" 28 | 29 | var goVersionRegex = regexp.MustCompile(`^go version go1.(\d+)`) 30 | 31 | func main() { 32 | // Parse command line arguments 33 | if len(os.Args) <= 1 { 34 | errorf("%s", usage) 35 | } 36 | 37 | var begin []string 38 | var end []string 39 | var perRecord []string 40 | fieldSep := " " 41 | printSource := false 42 | goExe := "go" 43 | 44 | for i := 1; i < len(os.Args); { 45 | arg := os.Args[i] 46 | i++ 47 | 48 | switch arg { 49 | case "-b": 50 | if i >= len(os.Args) { 51 | errorf("-b requires an argument") 52 | } 53 | begin = append(begin, os.Args[i]) 54 | i++ 55 | case "-e": 56 | if i >= len(os.Args) { 57 | errorf("-e requires an argument") 58 | } 59 | end = append(end, os.Args[i]) 60 | i++ 61 | case "-F": 62 | if i >= len(os.Args) { 63 | errorf("-F requires an argument") 64 | } 65 | fieldSep = os.Args[i] 66 | i++ 67 | case "-g": 68 | if i >= len(os.Args) { 69 | errorf("-g requires an argument") 70 | } 71 | goExe = os.Args[i] 72 | i++ 73 | case "-i": 74 | imports[os.Args[i]] = struct{}{} 75 | if i >= len(os.Args) { 76 | errorf("-i requires an argument") 77 | } 78 | i++ 79 | case "-h", "--help": 80 | fmt.Printf("%s\n", usage) 81 | return 82 | case "-s": 83 | printSource = true 84 | case "-V", "--version": 85 | fmt.Println(version) 86 | return 87 | default: 88 | switch { 89 | case strings.HasPrefix(arg, "-F"): 90 | fieldSep = arg[2:] 91 | default: 92 | perRecord = append(perRecord, arg) 93 | } 94 | } 95 | } 96 | 97 | if len(fieldSep) > 1 { 98 | _, err := regexp.Compile(fieldSep) 99 | if err != nil { 100 | errorf("invalid field separator: %v", err) 101 | } 102 | } 103 | 104 | // Use non-generic Sort/SortMap if importspkg.Process doesn't support 105 | // generics, or we're using a Go that doesn't support generics (<=1.17). 106 | sortFuncs := sortGeneric 107 | cmd := exec.Command(goExe, "version") 108 | output, err := cmd.CombinedOutput() 109 | if err == nil { 110 | matches := goVersionRegex.FindSubmatch(output) 111 | if matches != nil { 112 | goMinor, _ := strconv.Atoi(string(matches[1])) 113 | if goMinor <= 17 { 114 | sortFuncs = sortNonGeneric 115 | } 116 | } 117 | } 118 | _, err = importspkg.Process("", []byte("package x\nfunc f[T any]() {}"), nil) 119 | if err != nil { 120 | sortFuncs = sortNonGeneric 121 | } 122 | 123 | // Write source code to buffer 124 | var buffer bytes.Buffer 125 | params := &templateParams{ 126 | FieldSep: fieldSep, 127 | Imports: imports, 128 | Begin: begin, 129 | PerRecord: perRecord, 130 | End: end, 131 | SortFuncs: sortFuncs, 132 | } 133 | err = sourceTemplate.Execute(&buffer, params) 134 | if err != nil { 135 | errorf("error executing template: %v", err) 136 | } 137 | bufferBytes := buffer.Bytes() 138 | 139 | // Add imports (also pretty-prints for printSource mode). 140 | sourceBytes, err := importspkg.Process("", bufferBytes, nil) 141 | if err != nil { 142 | parsed := parseErrors(err.Error(), string(bufferBytes), params) 143 | fmt.Fprint(os.Stderr, parsed) 144 | os.Exit(1) 145 | } 146 | if printSource { 147 | fmt.Print(string(sourceBytes)) 148 | return 149 | } 150 | 151 | // Create a temporary work directory and .go file 152 | tempDir, err := os.MkdirTemp("", "prig_") 153 | if err != nil { 154 | errorf("error creating temp dir: %v", err) 155 | } 156 | defer os.RemoveAll(tempDir) 157 | goFilename := filepath.Join(tempDir, "main.go") 158 | err = os.WriteFile(goFilename, sourceBytes, 0666) 159 | if err != nil { 160 | errorf("error writing temp file: %v", err) 161 | } 162 | 163 | // Ensure that Go is installed 164 | _, err = exec.LookPath(goExe) 165 | if err != nil { 166 | errorf("You must install Go to use 'prig', see https://go.dev/doc/install") 167 | } 168 | 169 | // Build the program with "go build" 170 | exeFilename := filepath.Join(tempDir, "main") 171 | if runtime.GOOS == "windows" { 172 | exeFilename += ".exe" 173 | } 174 | cmd = exec.Command(goExe, "build", "-o", exeFilename, goFilename) 175 | output, err = cmd.CombinedOutput() 176 | switch err.(type) { 177 | case nil: 178 | case *exec.ExitError: 179 | parsed := parseErrors(string(output), string(sourceBytes), params) 180 | fmt.Fprint(os.Stderr, parsed) 181 | os.Exit(1) 182 | default: 183 | errorf("error building program: %v", err) 184 | } 185 | 186 | // Then run the executable we just built 187 | cmd = exec.Command(exeFilename) 188 | cmd.Stdin = os.Stdin 189 | cmd.Stdout = os.Stdout 190 | cmd.Stderr = os.Stderr 191 | err = cmd.Run() 192 | if err != nil { 193 | exitCode := cmd.ProcessState.ExitCode() 194 | if exitCode == -1 { 195 | errorf("error running program: %v", err) 196 | } 197 | os.Exit(exitCode) 198 | } 199 | } 200 | 201 | var compileErrorRe = regexp.MustCompile(`^(.*:)?(\d+):(\d+): (.*)`) 202 | 203 | func parseErrors(buildOutput string, source string, params *templateParams) string { 204 | var builder strings.Builder 205 | lines := strings.Split(buildOutput, "\n") 206 | for _, line := range lines { 207 | if line == "" || strings.HasPrefix(line, "# ") { 208 | continue 209 | } 210 | matches := compileErrorRe.FindStringSubmatch(line) 211 | if matches == nil { 212 | fmt.Fprintf(&builder, "%s\n", line) 213 | continue 214 | } 215 | lineNum, _ := strconv.Atoi(matches[2]) 216 | colNum, _ := strconv.Atoi(matches[3]) 217 | message := matches[4] 218 | sourceLine, caretLine := getSourceCaretLine(source, lineNum, colNum) 219 | fmt.Fprintf(&builder, "main.go:%d:%d: %s\n%s\n%s\n", lineNum, colNum, message, sourceLine, caretLine) 220 | } 221 | return builder.String() 222 | } 223 | 224 | func getSourceCaretLine(source string, line, col int) (sourceLine, caretLine string) { 225 | lines := strings.Split(source, "\n") 226 | if line < 1 || line > len(lines) { 227 | return "", "" 228 | } 229 | sourceLine = lines[line-1] 230 | numTabs := strings.Count(sourceLine[:col-1], "\t") 231 | runeColumn := utf8.RuneCountInString(sourceLine[:col-1]) 232 | sourceLine = strings.Replace(sourceLine, "\t", " ", -1) 233 | caretLine = strings.Repeat(" ", runeColumn) + strings.Repeat(" ", numTabs) + "^" 234 | return sourceLine, caretLine 235 | } 236 | 237 | func errorf(format string, args ...interface{}) { 238 | fmt.Fprintf(os.Stderr, format+"\n", args...) 239 | os.Exit(1) 240 | } 241 | 242 | const usage = `Prig ` + version + ` - Copyright (c) 2022 Ben Hoyt 243 | 244 | Usage: prig [options] [-b 'begin code'] 'per-record code' [-e 'end code'] 245 | 246 | Prig is for Processing Records In Go. It's like AWK, but snobbish (Go! static 247 | typing!). It runs 'begin code' first, then runs 'per-record code' for every 248 | record (line) in the input, then runs 'end code'. Prig uses "go build", so it 249 | requires the Go compiler: https://go.dev/doc/install 250 | 251 | Options: 252 | -F char | re field separator (single character or multi-char regex) 253 | -g executable Go compiler to use (eg: "go1.18rc1", default "go") 254 | -h, --help print help message and exit 255 | -i import import Go package (normally automatic) 256 | -s print formatted Go source instead of running 257 | -V, --version print version number and exit 258 | 259 | Built-in functions: 260 | F(i int) float64 // return field i as float64, int, or string 261 | I(i int) int // (i==0 is entire record, i==1 is first field) 262 | S(i int) string 263 | 264 | NF() int // return number of fields in current record 265 | NR() int // return number of current record 266 | 267 | Print(args ...interface{}) // fmt.Print, but buffered 268 | Printf(format string, args ...interface{}) // fmt.Printf, but buffered 269 | Println(args ...interface{}) // fmt.Println, but buffered 270 | 271 | Match(re, s string) bool // report whether s contains match of re 272 | Replace(re, s, repl string) string // replace all re matches in s with repl 273 | Submatches(re, s string) []string // return slice of submatches of re in s 274 | Substr(s string, n[, m] int) string // s[n:m] but safe and allow negative n/m 275 | 276 | Sort[T int|float64|string](s []T) []T 277 | // return new sorted slice; also Sort(s, Reverse) to sort descending 278 | SortMap[T int|float64|string](m map[string]T) []KV[T] 279 | // return sorted slice of key-value pairs 280 | // also SortMap(s[, Reverse][, ByValue]) to sort descending or by value 281 | 282 | Examples: 283 | # Run an arbitrary Go snippet; don't process input 284 | ` + exampleHelloWorld + ` 285 | 286 | # Print the average value of the last field 287 | ` + exampleAverage + ` 288 | 289 | # Print 3rd field in milliseconds if line contains "GET" or "HEAD" 290 | ` + exampleMilliseconds + ` 291 | 292 | # Print frequencies of unique words, most frequent first 293 | ` + exampleFrequencies 294 | 295 | // These are tested in prig_test.go to ensure we're testing our examples. 296 | const ( 297 | exampleHelloWorld = `prig -b 'Println("Hello, world!", math.Pi)'` 298 | exampleAverage = `prig -b 's := 0.0' 's += F(NF())' -e 'Println(s / float64(NR()))'` 299 | exampleMilliseconds = `prig 'if Match(` + "`" + `GET|HEAD` + "`" + `, S(0)) { Printf("%.0fms\n", F(3)*1000) }'` 300 | exampleFrequencies = `prig -b 'freqs := map[string]int{}' \ 301 | 'for i := 1; i <= NF(); i++ { freqs[strings.ToLower(S(i))]++ }' \ 302 | -e 'for _, f := range SortMap(freqs, ByValue, Reverse) { ' \ 303 | -e 'Println(f.K, f.V) }'` 304 | ) 305 | 306 | var imports = map[string]struct{}{ 307 | "bufio": {}, 308 | "fmt": {}, 309 | "os": {}, 310 | "regexp": {}, 311 | "sort": {}, 312 | "strconv": {}, 313 | "strings": {}, 314 | } 315 | 316 | type templateParams struct { 317 | FieldSep string 318 | Imports map[string]struct{} 319 | Begin []string 320 | PerRecord []string 321 | End []string 322 | SortFuncs string 323 | } 324 | 325 | var sourceTemplate = template.Must(template.New("source").Parse(`// Code generated by Prig (https://github.com/benhoyt/prig). DO NOT EDIT. 326 | 327 | package main 328 | 329 | import ( 330 | {{range $imp, $_ := .Imports}} 331 | {{- printf "%q" $imp}} 332 | {{end -}} 333 | ) 334 | 335 | var ( 336 | _output *bufio.Writer 337 | _record string 338 | _nr int 339 | _fields []string 340 | ) 341 | 342 | func main() { 343 | _output = bufio.NewWriter(os.Stdout) 344 | defer _output.Flush() 345 | 346 | {{range .Begin}} 347 | {{. -}} 348 | {{end}} 349 | 350 | {{if or .PerRecord .End}} 351 | _scanner := bufio.NewScanner(os.Stdin) 352 | for _scanner.Scan() { 353 | _record = _scanner.Text() 354 | _nr++ 355 | _fields = nil 356 | 357 | {{range .PerRecord}} 358 | {{. -}} 359 | {{end}} 360 | } 361 | if _scanner.Err() != nil { 362 | _errorf("error reading stdin: %v", _scanner.Err()) 363 | } 364 | {{end}} 365 | 366 | {{range .End}} 367 | {{. -}} 368 | {{end}} 369 | } 370 | 371 | func Print(args ...interface{}) { 372 | _, err := fmt.Fprint(_output, args...) 373 | if err != nil { 374 | _errorf("error writing output: %v", err) 375 | } 376 | } 377 | 378 | func Printf(format string, args ...interface{}) { 379 | _, err := fmt.Fprintf(_output, format, args...) 380 | if err != nil { 381 | _errorf("error writing output: %v", err) 382 | } 383 | } 384 | 385 | func Println(args ...interface{}) { 386 | _, err := fmt.Fprintln(_output, args...) 387 | if err != nil { 388 | _errorf("error writing output: %v", err) 389 | } 390 | } 391 | 392 | func NR() int { 393 | return _nr 394 | } 395 | 396 | func S(i int) string { 397 | if i == 0 { 398 | return _record 399 | } 400 | _ensureFields() 401 | if i < 1 || i > len(_fields) { 402 | return "" 403 | } 404 | return _fields[i-1] 405 | } 406 | 407 | func I(i int) int { 408 | s := S(i) 409 | n, err := strconv.Atoi(s) 410 | if err != nil { 411 | f, _ := strconv.ParseFloat(s, 64) 412 | return int(f) 413 | } 414 | return n 415 | } 416 | 417 | func F(i int) float64 { 418 | s := S(i) 419 | f, _ := strconv.ParseFloat(s, 64) 420 | return f 421 | } 422 | 423 | var _fieldSepRegex *regexp.Regexp 424 | 425 | func _ensureFields() { 426 | if _fields != nil { 427 | return 428 | } 429 | {{if eq .FieldSep " "}} 430 | _fields = strings.Fields(_record) 431 | {{else}} 432 | if _record == "" { 433 | _fields = []string{} 434 | return 435 | } 436 | {{if le (len .FieldSep) 1}} 437 | _fields = strings.Split(_record, {{printf "%q" .FieldSep}}) 438 | {{else}} 439 | if _fieldSepRegex == nil { 440 | _fieldSepRegex = regexp.MustCompile({{printf "%q" .FieldSep}}) 441 | } 442 | _fields = _fieldSepRegex.Split(_record, -1) 443 | {{end}} 444 | {{end}} 445 | } 446 | 447 | func NF() int { 448 | _ensureFields() 449 | return len(_fields) 450 | } 451 | 452 | func Match(re, s string) bool { 453 | regex := _reCompile(re) 454 | return regex.MatchString(s) 455 | } 456 | 457 | func Replace(re, s, repl string) string { 458 | regex := _reCompile(re) 459 | return regex.ReplaceAllString(s, repl) 460 | } 461 | 462 | func Submatches(re, s string) []string { 463 | regex := _reCompile(re) 464 | matches := regex.FindStringSubmatch(s) 465 | if matches == nil { 466 | return nil 467 | } 468 | return matches[1:] 469 | } 470 | 471 | var _reCache = make(map[string]*regexp.Regexp) 472 | 473 | func _reCompile(re string) *regexp.Regexp { 474 | if regex, ok := _reCache[re]; ok { 475 | return regex 476 | } 477 | regex, err := regexp.Compile(re) 478 | if err != nil { 479 | _errorf("invalid regex %q: %v", re, err) 480 | } 481 | // Dumb, non-LRU cache: just cache the first 100 regexes 482 | if len(_reCache) < 100 { 483 | _reCache[re] = regex 484 | } 485 | return regex 486 | } 487 | 488 | func Substr(s string, n int, ms ...int) string { 489 | var m int 490 | switch len(ms) { 491 | case 0: 492 | m = len(s) 493 | case 1: 494 | m = ms[0] 495 | default: 496 | _errorf("Substr takes 2 or 3 arguments, not %d", len(ms)+2) 497 | } 498 | 499 | if n < 0 { 500 | n = len(s) + n 501 | if n < 0 { 502 | n = 0 503 | } 504 | } 505 | if n > len(s) { 506 | n = len(s) 507 | } 508 | 509 | if m < 0 { 510 | m = len(s) + m 511 | if m < 0 { 512 | m = 0 513 | } 514 | } 515 | if m > len(s) { 516 | m = len(s) 517 | } 518 | 519 | if n > m { 520 | return "" 521 | } 522 | 523 | return s[n:m] 524 | } 525 | 526 | type _sortOption int 527 | 528 | const ( 529 | Reverse _sortOption = iota 530 | ByValue 531 | ) 532 | 533 | func _getSortOptions(options ..._sortOption) (reverse bool) { 534 | for _, option := range options { 535 | switch option { 536 | case Reverse: 537 | reverse = true 538 | case ByValue: 539 | _errorf("Sort option ByValue not valid") 540 | default: 541 | _errorf("Sort option %d valid", option) 542 | } 543 | } 544 | return reverse 545 | } 546 | 547 | func _getSortMapOptions(options ..._sortOption) (reverse, byValue bool) { 548 | for _, option := range options { 549 | switch option { 550 | case Reverse: 551 | reverse = true 552 | case ByValue: 553 | byValue = true 554 | default: 555 | _errorf("SortMap option %d not valid", option) 556 | } 557 | } 558 | return reverse, byValue 559 | } 560 | 561 | {{.SortFuncs}} 562 | 563 | func _errorf(format string, args ...interface{}) { 564 | fmt.Fprintf(os.Stderr, format+"\n", args...) 565 | os.Exit(1) 566 | } 567 | `)) 568 | 569 | const sortGeneric = ` 570 | func Sort[T int|float64|string](s []T, options ..._sortOption) []T { 571 | reverse := _getSortOptions(options...) 572 | 573 | // TODO: probably could be improved when slices package arrives 574 | result := make([]T, len(s)) 575 | copy(result, s) 576 | if reverse { 577 | sort.Slice(result, func(i, j int) bool { 578 | return result[i] > result[j] 579 | }) 580 | } else { 581 | sort.Slice(result, func(i, j int) bool { 582 | return result[i] < result[j] 583 | }) 584 | } 585 | return result 586 | } 587 | 588 | type KV[T int|float64|string] struct { 589 | K string 590 | V T 591 | } 592 | 593 | func SortMap[T int|float64|string](m map[string]T, options ..._sortOption) []KV[T] { 594 | reverse, byValue := _getSortMapOptions(options...) 595 | 596 | kvs := make([]KV[T], 0, len(m)) 597 | for k, v := range m { 598 | kvs = append(kvs, KV[T]{k, v}) 599 | } 600 | 601 | // TODO: probably could be improved when slices package arrives 602 | if byValue { 603 | sort.Slice(kvs, func (i, j int) bool { 604 | if kvs[i].V == kvs[j].V { 605 | return kvs[i].K < kvs[j].K 606 | } 607 | return kvs[i].V < kvs[j].V 608 | }) 609 | } else { 610 | sort.Slice(kvs, func (i, j int) bool { 611 | if kvs[i].K == kvs[j].K { 612 | return kvs[i].V < kvs[j].V 613 | } 614 | return kvs[i].K < kvs[j].K 615 | }) 616 | } 617 | 618 | if reverse { 619 | for i, j := 0, len(kvs)-1; i < len(kvs)/2; i, j = i+1, j-1 { 620 | tmp := kvs[i] 621 | kvs[i] = kvs[j] 622 | kvs[j] = tmp 623 | } 624 | } 625 | return kvs 626 | } 627 | ` 628 | 629 | const sortNonGeneric = ` 630 | func Sort(s interface{}, options ..._sortOption) []interface{} { 631 | reverse := _getSortOptions(options...) 632 | 633 | var result []interface{} 634 | switch s := s.(type) { 635 | case []int: 636 | cp := make([]int, len(s)) 637 | copy(cp, s) 638 | sort.Ints(cp) 639 | result = make([]interface{}, len(s)) 640 | for i, x := range cp { 641 | result[i] = x 642 | } 643 | case []float64: 644 | cp := make([]float64, len(s)) 645 | copy(cp, s) 646 | sort.Float64s(cp) 647 | result = make([]interface{}, len(s)) 648 | for i, x := range cp { 649 | result[i] = x 650 | } 651 | case []string: 652 | cp := make([]string, len(s)) 653 | copy(cp, s) 654 | sort.Strings(cp) 655 | result = make([]interface{}, len(s)) 656 | for i, x := range cp { 657 | result[i] = x 658 | } 659 | default: 660 | _errorf("Sort type must be int, float64, or string") 661 | } 662 | 663 | if reverse { 664 | for i, j := 0, len(result)-1; i < len(result)/2; i, j = i+1, j-1 { 665 | tmp := result[i] 666 | result[i] = result[j] 667 | result[j] = tmp 668 | } 669 | } 670 | return result 671 | } 672 | 673 | type KV struct { 674 | K string 675 | V interface{} 676 | } 677 | 678 | func SortMap(m interface{}, options ..._sortOption) []KV { 679 | reverse, byValue := _getSortMapOptions(options...) 680 | 681 | var kvs []KV 682 | var vLess func(i, j int) bool 683 | switch m := m.(type) { 684 | case map[string]int: 685 | kvs = make([]KV, 0, len(m)) 686 | for k, v := range m { 687 | kvs = append(kvs, KV{k, v}) 688 | } 689 | vLess = func(i, j int) bool { 690 | return kvs[i].V.(int) < kvs[j].V.(int) 691 | } 692 | case map[string]float64: 693 | kvs = make([]KV, 0, len(m)) 694 | for k, v := range m { 695 | kvs = append(kvs, KV{k, v}) 696 | } 697 | vLess = func(i, j int) bool { 698 | return kvs[i].V.(float64) < kvs[j].V.(float64) 699 | } 700 | case map[string]string: 701 | kvs = make([]KV, 0, len(m)) 702 | for k, v := range m { 703 | kvs = append(kvs, KV{k, v}) 704 | } 705 | vLess = func(i, j int) bool { 706 | return kvs[i].V.(string) < kvs[j].V.(string) 707 | } 708 | default: 709 | _errorf("SortMap values must be int, float64, or string") 710 | } 711 | 712 | if byValue { 713 | sort.Slice(kvs, func (i, j int) bool { 714 | if kvs[i].V == kvs[j].V { 715 | return kvs[i].K < kvs[j].K 716 | } 717 | return vLess(i, j) 718 | }) 719 | } else { 720 | sort.Slice(kvs, func (i, j int) bool { 721 | if kvs[i].K == kvs[j].K { 722 | return vLess(i, j) 723 | } 724 | return kvs[i].K < kvs[j].K 725 | }) 726 | } 727 | 728 | if reverse { 729 | for i, j := 0, len(kvs)-1; i < len(kvs)/2; i, j = i+1, j-1 { 730 | tmp := kvs[i] 731 | kvs[i] = kvs[j] 732 | kvs[j] = tmp 733 | } 734 | } 735 | return kvs 736 | } 737 | ` 738 | --------------------------------------------------------------------------------