├── .gitignore ├── testdata ├── favicon.ico ├── go-favicon-0.png ├── go-favicon-1.png ├── go-logo-blue.png ├── Makefile └── go-logo-blue.svg ├── .config └── mise.toml ├── sub ├── .spectral.yml ├── Makefile ├── share │ ├── doc.go │ └── share.go ├── doc.go ├── play │ ├── doc.go │ ├── play.go │ └── play_test.go └── playground-openapi.yaml ├── go.mod ├── go.sum ├── useragent_test.go ├── online_test.go ├── .github └── workflows │ └── go.yml ├── sub_offline.go ├── useragent_buildinfo.go ├── doc.go ├── sub.go ├── main_test.go ├── README.md ├── LICENSE └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | /goeval 2 | -------------------------------------------------------------------------------- /testdata/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dolmen-go/goeval/HEAD/testdata/favicon.ico -------------------------------------------------------------------------------- /testdata/go-favicon-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dolmen-go/goeval/HEAD/testdata/go-favicon-0.png -------------------------------------------------------------------------------- /testdata/go-favicon-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dolmen-go/goeval/HEAD/testdata/go-favicon-1.png -------------------------------------------------------------------------------- /testdata/go-logo-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dolmen-go/goeval/HEAD/testdata/go-logo-blue.png -------------------------------------------------------------------------------- /.config/mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | "aqua:golangci/golangci-lint" = "2.1.2" 3 | "aqua:stoplightio/spectral" = "6.14.3" 4 | node = "23.11.0" 5 | "npm:openapicmd" = "2.6.1" 6 | -------------------------------------------------------------------------------- /sub/.spectral.yml: -------------------------------------------------------------------------------- 1 | # OpenAPI rules 2 | # Reference: https://meta.stoplight.io/docs/spectral/ZG9jOjExNw-open-api-rules 3 | extends: "spectral:oas" 4 | rules: 5 | contact-properties: off 6 | operation-tags: off 7 | -------------------------------------------------------------------------------- /testdata/Makefile: -------------------------------------------------------------------------------- 1 | 2 | 3 | .PHONY: all 4 | 5 | all: go-logo-blue.png 6 | 7 | %.png: %.svg 8 | magick -background none $< -colors 2 $@ 9 | 10 | go-logo-blue.svg: 11 | curl -O https://go.dev/images/go-logo-blue.svg 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dolmen-go/goeval 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | golang.org/x/mod v0.29.0 7 | golang.org/x/tools v0.38.0 8 | ) 9 | 10 | require golang.org/x/sync v0.17.0 // indirect 11 | 12 | tool github.com/dolmen-go/goeval 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 2 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 3 | golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= 4 | golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= 5 | golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= 6 | golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 7 | golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= 8 | golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= 9 | -------------------------------------------------------------------------------- /sub/Makefile: -------------------------------------------------------------------------------- 1 | # Tools for playground-openapi.yaml maintenance. 2 | # 3 | # Required tools: 4 | # - docker 5 | # - mise (see ../.config/mise.toml) https://mise.jdx.dev/ 6 | # 7 | # All other tools are installed via mise. 8 | 9 | .PHONY: openapi-validate 10 | 11 | openapi-validate: playground-openapi.yaml .spectral.yml 12 | docker run --rm -it -v "$(CURDIR):/src" stoplight/spectral lint -r /src/.spectral.yml /src/playground-openapi.yaml 13 | 14 | .PHONY: swagger-ui 15 | 16 | PORT ?= 8080 17 | 18 | swagger-ui: 19 | ( sleep 2 ; open http://localhost:$(PORT)/ ) & 20 | docker run --rm -p 127.0.0.1:$(PORT):8080 -v "$(CURDIR):/src" -e SWAGGER_JSON=/src/playground-openapi.yaml swaggerapi/swagger-ui 21 | -------------------------------------------------------------------------------- /sub/share/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Olivier Mengué. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Command share is the sub command launched by "goeval -share". 18 | package main 19 | -------------------------------------------------------------------------------- /sub/share/share.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | "os" 8 | ) 9 | 10 | type uaTransport struct { 11 | rt http.RoundTripper 12 | UserAgent string 13 | } 14 | 15 | func (t *uaTransport) RoundTrip(req *http.Request) (*http.Response, error) { 16 | req.Header.Set("User-Agent", t.UserAgent) 17 | return t.rt.RoundTrip(req) 18 | } 19 | 20 | func main() { 21 | http.DefaultTransport = &uaTransport{rt: http.DefaultTransport, UserAgent: os.Args[1]} 22 | 23 | resp, err := http.Post("https://play.golang.org/share", "text/plain; charset=utf-8", os.Stdin) 24 | if err != nil { 25 | log.Fatal("share:", err) 26 | } 27 | defer resp.Body.Close() 28 | id, err := io.ReadAll(resp.Body) 29 | if err != nil { 30 | log.Fatal("share:", err) 31 | } 32 | io.WriteString(os.Stdout, "https://go.dev/play/p/"+string(id)+"\n") 33 | } 34 | -------------------------------------------------------------------------------- /useragent_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Olivier Mengué. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "runtime/debug" 21 | "testing" 22 | ) 23 | 24 | // Run this test with "go test -buildvcs=true" 25 | func TestShowUserAgent(t *testing.T) { 26 | bi, ok := debug.ReadBuildInfo() 27 | if ok { 28 | t.Log("version from BuildInfo:", bi.Main.Version) 29 | if bi.Main.Version[0] != 'v' { 30 | t.Log("\033[1;31mRun this test with -buildvcs=true\033[m") 31 | } 32 | t.Logf("%#v", bi) 33 | } 34 | 35 | t.Log("User-Agent:", getUserAgent()) 36 | } 37 | -------------------------------------------------------------------------------- /online_test.go: -------------------------------------------------------------------------------- 1 | //go:build !goeval.offline 2 | 3 | /* 4 | Copyright 2025 Olivier Mengué. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package main_test 20 | 21 | // Show "goeval -play": running code remotely on https://play.golang.org/ 22 | func Example_play() { 23 | goeval(`-play`, `fmt.Println(time.Now())`) 24 | 25 | // Output: 26 | // 2009-11-10 23:00:00 +0000 UTC m=+0.000000001 27 | } 28 | 29 | // Show "goeval -play", with arguments values sent with the program code 30 | func Example_playWithArgs() { 31 | goeval(`-play`, `fmt.Println(os.Args[1])`, `toto`) 32 | 33 | // Output: 34 | // toto 35 | } 36 | -------------------------------------------------------------------------------- /sub/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Olivier Mengué. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package sub contains sub commands of [github.com/dolmen-go/goeval]. 18 | // 19 | // Sub commands have the following constraints: 20 | // - only stdlib dependencies 21 | // - compiled in GOPATH mode (GO111MODULE=off) 22 | // 23 | // The source code of each command is embedded (see [embed]) in the goeval binary (see ../sub.go) 24 | // and commands are launched with "go run". This allows to keep the goeval binary lightweight 25 | // ([net/http] and the crypto stack are not bundled in the main binary). 26 | package sub 27 | -------------------------------------------------------------------------------- /sub/play/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Olivier Mengué. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Command play is the sub command launched by "goeval -play". 18 | // 19 | // play sends the program code to the Go Playground at https://play.golang.org/compile 20 | // and replays the received events respecting event delays. 21 | // 22 | // $ curl -s -X POST --data-urlencode body@- https://play.golang.org/compile < -------------------------------------------------------------------------------- /sub/play/play.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "log" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | "time" 11 | ) 12 | 13 | type uaTransport struct { 14 | rt http.RoundTripper 15 | UserAgent string 16 | } 17 | 18 | func (t *uaTransport) RoundTrip(req *http.Request) (*http.Response, error) { 19 | req.Header.Set("User-Agent", t.UserAgent) 20 | return t.rt.RoundTrip(req) 21 | } 22 | 23 | func main() { 24 | http.DefaultTransport = &uaTransport{rt: http.DefaultTransport, UserAgent: os.Args[1]} 25 | 26 | code, _ := io.ReadAll(os.Stdin) 27 | resp, err := http.PostForm("https://play.golang.org/compile", url.Values{"body": {string(code)}}) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | defer resp.Body.Close() 32 | // resp.Body = io.NopCloser(io.TeeReader(resp.Body, os.Stdout)); // Enable for debugging 33 | var r struct { 34 | Errors string 35 | Events []struct { 36 | Delay time.Duration 37 | Message string 38 | Kind string 39 | } 40 | Status int 41 | // IsTest bool // unused 42 | // TestsFailed int // unused 43 | } 44 | if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { 45 | log.Fatal(err) 46 | } 47 | if r.Errors != "" { 48 | log.Print(r.Errors) 49 | } 50 | // Replay events 51 | for _, ev := range r.Events { 52 | time.Sleep(ev.Delay) 53 | if ev.Kind == "stdout" { 54 | io.WriteString(os.Stdout, ev.Message) 55 | } else { 56 | io.WriteString(os.Stderr, ev.Message) 57 | } 58 | } 59 | os.Exit(r.Status) 60 | } 61 | -------------------------------------------------------------------------------- /sub/play/play_test.go: -------------------------------------------------------------------------------- 1 | //go:build !goeval.offline 2 | 3 | /* 4 | Copyright 2025 Olivier Mengué. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package main_test 20 | 21 | import ( 22 | "os" 23 | "os/exec" 24 | "strings" 25 | ) 26 | 27 | const userAgent = "goeval.play.test/v0.0.0 (github.com/dolmen-go/goeval/sub/play_test)" 28 | 29 | func Example_fmt() { 30 | cmd := exec.Command("go", "run", "play.go", userAgent) 31 | cmd.Env = append(os.Environ(), "GO111MODULE=off") 32 | cmd.Stdin = strings.NewReader(`package main;import"fmt";func main(){fmt.Println("OK")}`) 33 | cmd.Stdout = os.Stdout 34 | cmd.Run() 35 | 36 | // Output: 37 | // OK 38 | } 39 | 40 | func Example_time() { 41 | cmd := exec.Command("go", "run", "play.go", userAgent) 42 | cmd.Env = append(os.Environ(), "GO111MODULE=off") 43 | cmd.Stdin = strings.NewReader(`package main;import("fmt";"time");func main(){fmt.Println(time.Now().Format(time.RFC3339))}`) 44 | cmd.Stdout = os.Stdout 45 | cmd.Run() 46 | 47 | // Output: 48 | // 2009-11-10T23:00:00Z 49 | } 50 | -------------------------------------------------------------------------------- /useragent_buildinfo.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 Olivier Mengué. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "runtime/debug" 21 | "strings" 22 | ) 23 | 24 | var version = "v1.4.0" // FIXME set at compile time with -ldflags="-X main.version=" 25 | 26 | // getUserAgent returns the HTTP User-Agent header value to use for -play and -share. 27 | // 28 | // Reference: https://www.rfc-editor.org/rfc/rfc9110#name-user-agent 29 | func getUserAgent() string { 30 | bi, ok := debug.ReadBuildInfo() 31 | if !ok { 32 | // The HTTP specification allows comments in header values: they are enclosed by parenthesis. 33 | return "goeval/" + version + " (github.com/dolmen-go/goeval)" 34 | } 35 | if !ok || bi.Main.Path == "" { 36 | // The HTTP specification allows comments in header values: they are enclosed by parenthesis. 37 | return "goeval/" + version + " (" + bi.Path + ")" 38 | } 39 | // "go build" with -buildvcs=false (default for "go run", "go test") reports "(devel)" as version 40 | // but in header value parenthesis are reserved chars (for comments). 41 | return "goeval/" + strings.Trim(bi.Main.Version, "()") + " (" + bi.Main.Path + ")" 42 | } 43 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2025 Olivier Mengué. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Command goeval allows to run Go snippets given on the command line. 18 | // 19 | // A Go toolchain must be available in $PATH as goeval relies on "go run". 20 | // 21 | // The code, given either as the first argument or on stdin, is wrapped as 22 | // the body of a main() function in a main package, and executed with "go run". 23 | // 24 | // Imports are implicit (they are usually resolved automatically thanks to 25 | // [goimports]) but they can be explicitely specified using -i. 26 | // If at least one package import is given with a version (import-path@version), 27 | // a full Go module is assembled, and imports without version are resolved 28 | // as the latest version available in the local Go module cache (GOMODCACHE). 29 | // 30 | // In GOPATH mode (the default), the local Go context is involved only if the current 31 | // directory happens to be in GOPATH and the package is imported. 32 | // In Go module mode, the local Go context (go.mod, .go source files) is completely 33 | // ignored for resolving imports and compiling the snippet. 34 | // 35 | // -play runs the code in the sandbox of [the Go Playground] instead of the local 36 | // machine and replays the output. 37 | // 38 | // -share posts the code for storage on [the Go Playground] and displays the URL. 39 | // 40 | // 🚀 Quick Start 41 | // 42 | // go install github.com/dolmen-go/goeval@latest 43 | // goeval 'fmt.Println("Hello, world")' 44 | // 45 | // [goimports]: https://pkg.go.dev/golang.org/x/tools/imports 46 | // [the Go Playground]: https://go.dev/play 47 | package main 48 | -------------------------------------------------------------------------------- /sub.go: -------------------------------------------------------------------------------- 1 | //go:build !goeval.offline 2 | 3 | /* 4 | Copyright 2025 Olivier Mengué. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | package main 20 | 21 | import ( 22 | "bytes" 23 | _ "embed" 24 | "io" 25 | "log" 26 | "os" 27 | "os/exec" 28 | ) 29 | 30 | func registerOnlineFlags() { 31 | // TODO allow to optionally set a different endpoint 32 | flagAction("play", actionPlay, nil, "run the code remotely on https://go.dev/play") 33 | flagAction("share", actionShare, nil, "share the code on https://go.dev/play and print the URL.") 34 | } 35 | 36 | var ( 37 | //go:embed sub/play/play.go 38 | playClient string 39 | //go:embed sub/share/share.go 40 | shareClient string 41 | ) 42 | 43 | // prepareSubPlay prepare the source code for compilation and execution of sub/play/play.go. 44 | func prepareSubPlay() (stdin *bytes.Buffer, tail func() error, cleanup func()) { 45 | return prepareSub(playClient) 46 | } 47 | 48 | // prepareSubPlay prepare the source code for compilation and execution of sub/share/share.go. 49 | func prepareSubShare() (stdin *bytes.Buffer, tail func() error, cleanup func()) { 50 | return prepareSub(shareClient) 51 | } 52 | 53 | // prepareSub prepares execution of a sub command via a "go run". 54 | // The returned stdin buffer may be filled with data. 55 | // cleanup must be called after cmd.Run() to clean the tempoary go source created. 56 | func prepareSub(appCode string) (stdin *bytes.Buffer, tail func() error, cleanup func()) { 57 | f, err := os.CreateTemp("", "*.go") 58 | if err != nil { 59 | log.Fatal(err) 60 | } 61 | defer f.Close() 62 | fName := f.Name() 63 | cleanup = func() { 64 | os.Remove(fName) 65 | } 66 | 67 | if _, err := io.WriteString(f, appCode); err != nil { 68 | log.Fatal(err) 69 | } 70 | 71 | // Prepare input that will be filled before executing the command 72 | stdin = new(bytes.Buffer) 73 | 74 | // Run "go run" with the code submitted on stdin and the userAgent as first argument 75 | cmd := exec.Command(goCmd, "run", fName, getUserAgent()) 76 | cmd.Env = append( 77 | os.Environ(), // We must not use the 'env' built for local run here 78 | "GO111MODULE=off", // Sub command use only stdlib 79 | "GOEXPERIMENT=", // Clear GOEXPERIMENT which has been forwarded in a comment 80 | ) 81 | cmd.Stdin = stdin 82 | cmd.Stdout = os.Stdout 83 | cmd.Stderr = os.Stderr 84 | 85 | tail = func() error { 86 | return run(cmd) 87 | } 88 | return 89 | } 90 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2025 Olivier Mengué. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main_test 18 | 19 | import ( 20 | "bytes" 21 | "os" 22 | "os/exec" 23 | "path/filepath" 24 | "runtime" 25 | "testing" 26 | ) 27 | 28 | func goeval(args ...string) { 29 | cmd := exec.Command("go", append([]string{"run", "."}, args...)...) 30 | cmd.Stdin = nil 31 | cmd.Stdout = os.Stdout 32 | cmd.Stderr = os.Stderr 33 | cmd.Run() 34 | } 35 | 36 | func Example() { 37 | goeval(`fmt.Println("OK")`) 38 | 39 | // Output: 40 | // OK 41 | } 42 | 43 | func Example_dump() { 44 | goeval("-E", `fmt.Println("OK")`) 45 | 46 | // Output: 47 | // package main 48 | // 49 | // import "fmt" 50 | // 51 | // func main() { 52 | // //line :1 53 | // fmt.Println("OK") 54 | // } 55 | } 56 | 57 | func Example_flag() { 58 | goeval(`fmt.Println(os.Args[1])`, `--`) 59 | goeval(`fmt.Println(os.Args[1])`, `-x`) // -x is also a "go run" flag 60 | goeval(`fmt.Println(os.Args[1])`, `toto.go`) // toto.go could be caught by "go run" 61 | 62 | // Output: 63 | // -- 64 | // -x 65 | // toto.go 66 | } 67 | 68 | func Example_import() { 69 | goeval(`-goimports=`, `-i`, `fmt`, `fmt.Println("OK")`) 70 | goeval(`-goimports=`, `-i=fmt`, `fmt.Println("OK")`) 71 | goeval(`-goimports=`, `-i`, `fmt`, `-i`, `time`, `fmt.Println(time.Time{}.In(time.UTC))`) 72 | goeval(`-goimports=`, `-i`, `fmt,time`, `fmt.Println(time.Time{}.In(time.UTC))`) 73 | goeval(`-goimports=`, `-i=fmt,time`, `fmt.Println(time.Time{}.In(time.UTC))`) 74 | 75 | // Output: 76 | // OK 77 | // OK 78 | // 0001-01-01 00:00:00 +0000 UTC 79 | // 0001-01-01 00:00:00 +0000 UTC 80 | // 0001-01-01 00:00:00 +0000 UTC 81 | } 82 | 83 | // printlnWriter writes each line to a [fmt.Println]-like function. 84 | // [testing.T.Log] is such a function. 85 | type printlnWriter func(...any) 86 | 87 | func (tl printlnWriter) Write(b []byte) (int, error) { 88 | for len(b) > 0 { 89 | p := bytes.IndexByte(b, '\n') 90 | if p == -1 { 91 | tl(string(b)) 92 | break 93 | } 94 | line := b[:p] 95 | if len(line) > 1 && line[p-1] == '\r' { 96 | line = line[:p-1] 97 | } 98 | tl(string(line)) 99 | b = b[p+1:] 100 | } 101 | return len(b), nil 102 | } 103 | 104 | // goevalPrint runs goeval with the given arguments, and sends each line from standard output 105 | // to the stdout func (a [fmt.Println]-like func), and each line from standard error to the 106 | // stderr func. 107 | func goevalPrint(stdout func(...any), stderr func(...any), args ...string) { 108 | // As goeval is declared as a tool in go.mod (go get -tool .), we can call it as a tool. 109 | // "go tool" preserves the exit code while "go run" doesn't. 110 | cmd := exec.Command("go", append([]string{"tool", "goeval"}, args...)...) 111 | cmd.Stdin = nil 112 | cmd.Stdout = printlnWriter(stdout) 113 | cmd.Stderr = printlnWriter(stderr) 114 | cmd.Run() 115 | } 116 | 117 | // goevalT runs goeval with the given arguments, and sends each line from stdout to tb.Log 118 | // and each line from stderr to tb.Error. 119 | func goevalT(tb testing.TB, args ...string) { 120 | goevalPrint(tb.Log, tb.Error, args...) 121 | } 122 | 123 | func TestShowRuntimeBuildInfo(t *testing.T) { 124 | goevalT(t, `-i=fmt,runtime/debug,os`, `-goimports=`, `bi,ok:=debug.ReadBuildInfo(); if !ok {os.Exit(1)}; fmt.Print(bi)`) 125 | } 126 | 127 | func TestPrintStack(t *testing.T) { 128 | // PrintStack sends output to stderr 129 | goevalPrint(t.Log, t.Log, `-i=runtime/debug`, `-goimports=`, `debug.PrintStack()`) 130 | } 131 | 132 | // Test "goeval -o ..." 133 | func TestBuild(t *testing.T) { 134 | tempDir := t.TempDir() 135 | 136 | exe := filepath.Join(tempDir, "x") 137 | if runtime.GOOS == "windows" { 138 | exe += ".exe" 139 | } 140 | t.Logf("Building %q...", exe) 141 | 142 | goevalT(t, `-o`, exe, `fmt.Print(os.Args[1])`) 143 | 144 | _, err := os.Stat(exe) 145 | if err != nil { 146 | t.Fatalf(`%q: %v`, exe, err) 147 | } 148 | 149 | cmd := exec.Command(exe, `toto`) 150 | out, err := cmd.Output() 151 | if err != nil { 152 | t.Fatalf(`exec(%q): %v`, exe, err) 153 | } 154 | if string(out) != "toto" { 155 | t.Errorf(`output: got %q, expected "toto"`, out) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # goeval - Evaluate Go snippets instantly from the command line 3 | 4 | ## 🚀 Demo 5 | 6 | ```console 7 | $ goeval 'fmt.Println("Hello, world!")' 8 | Hello, world! 9 | $ goeval 'fmt.Println(os.Args[1])' 'Hello, world!' 10 | Hello, world! 11 | $ goeval -i .=fmt -i os 'Println(os.Args[1])' 'Hello, world!' 12 | Hello, world! 13 | $ goeval -i math/rand 'fmt.Println(rand.Int())' 14 | 5577006791947779410 15 | 16 | $ goeval -i fmt -i math/big -i os 'var x, y, z big.Int; x.SetString(os.Args[1], 10); y.SetString(os.Args[2], 10); fmt.Println(z.Mul(&x, &y).String())' 45673432245678899065433367889424354 136762347343433356789893322 17 | 6246405805150306996814033892780381988744339134177555648763988 18 | 19 | $ # Use os.Args 20 | $ goeval 'fmt.Printf("%x\n", sha256.Sum256([]byte(os.Args[1])))' abc 21 | ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad 22 | 23 | $ # Download module in GOPATH mode and use with goeval 24 | $ GO111MODULE=off go get github.com/klauspost/cpuid && goeval -i github.com/klauspost/cpuid/v2 'fmt.Println(cpuid.CPU.X64Level())' 25 | 3 26 | 27 | $ # Serve the current directory over HTTP 28 | $ goeval 'http.Handle("/",http.FileServer(http.Dir(".")));http.ListenAndServe(":8084",nil)' 29 | 30 | $ # Import net/http symbols in package scope for shorter code 31 | $ goeval -i .=net/http 'Handle("/",FileServer(Dir(".")));ListenAndServe(":8084",nil)' 32 | ``` 33 | 34 | ### Go modules 35 | 36 | Use `-i @` to import a Go module. 37 | 38 | Use `-i =@` to import a Go module and import the package with the given alias. 39 | 40 | ```console 41 | $ goeval -i .=github.com/bitfield/script@v0.24.1 'ListFiles("./[^.]*").First(4).Stdout()' 42 | LICENSE 43 | README.md 44 | doc.go 45 | go.mod 46 | 47 | $ goeval -i github.com/klauspost/cpuid/v2@v2.2.3 -i github.com/klauspost/cpuid/v2 'fmt.Println(cpuid.CPU.X64Level())' 48 | 3 49 | $ goeval -i cpuid=github.com/klauspost/cpuid/v2@v2.2.3 'fmt.Println(cpuid.CPU.X64Level())' 50 | 3 51 | ``` 52 | 53 | 58 | 59 | ### [go.dev/play](https://go.dev/play) 60 | 61 | Run your code on the Go Playground, and show output on the terminal: 62 | 63 | ```console 64 | $ goeval -play 'fmt.Println(time.Now())' 65 | 2009-11-10 23:00:00 +0000 UTC m=+0.000000001 66 | ``` 67 | 68 | Show the code sent to the Go Playground: 69 | 70 | ```console 71 | $ goeval -Eplay 'fmt.Println(time.Now())' 72 | package main 73 | 74 | import ( 75 | "fmt" 76 | "time" 77 | ) 78 | 79 | func main() { 80 | fmt.Println(time.Now()) 81 | } 82 | ``` 83 | 84 | Share the code on `go.dev/play`: 85 | ```console 86 | $ goeval -share 'fmt.Println(time.Now())' 87 | https://go.dev/play/p/Z35Vf8gIg4Z 88 | ``` 89 | 90 | Run on [`go.dev/play`](https://go.dev/play) with GOEXPERIMENT (the Go Playground enables GOEXPERIMENT via special comment): 91 | ```console 92 | $ GOEXPERIMENT=rangefunc goeval -play 'fmt.Println(runtime.Version())' 93 | go1.24.4 X:rangefunc 94 | ``` 95 | 96 | ## ⬇️ Install 97 | 98 | ```console 99 | $ go install github.com/dolmen-go/goeval@latest 100 | ``` 101 | 102 | Install with online features (`-play`, `-share`) disabled: 103 | 104 | ```console 105 | $ go install -tags=goeval.offline github.com/dolmen-go/goeval@latest 106 | ``` 107 | 108 | 109 | ## 🗑️ Uninstall 110 | 111 | ```console 112 | $ go clean -i github.com/dolmen-go/goeval 113 | ``` 114 | 115 | ## ❓ How does it work? 116 | 117 | ### GOPATH mode 118 | 119 | `goeval` just wraps your code with the necessary text to build a `main` package and a `main` func with the given imports, pass it through the [`goimports` tool](https://godoc.org/golang.org/x/tools/cmd/goimports) (to automatically add missing imports), writes in a temporary file and calls `go run` with [`GO111MODULE=off`](https://golang.org/ref/mod#mod-commands). 120 | 121 | `goimports` is enabled by default, but you can disable it to force explicit imports (for forward safety): 122 | 123 | ```console 124 | $ goeval -goimports= -i fmt 'fmt.Println("Hello, world!")' 125 | Hello, world! 126 | ``` 127 | 128 | ### Go module mode 129 | 130 | When at least one `module@version` is imported with `-i`, Go module mode is enabled. Two files are generated: `tmpxxxx.go` and `go.mod`. Then `go get .` is run to resolve and fetch dependencies, and then `go run`. 131 | 132 | ## 🛠️ Debugging 133 | 134 | To debug a syntax error: 135 | 136 | ```console 137 | $ goeval -E -goimports= ... | goimports 138 | ``` 139 | 140 | ## 🧙 Unsupported tricks 141 | 142 | Here are some tricks that have worked in the past, that may still work in the last version, but are not guaranteed to work later. 143 | 144 | ### Use functions 145 | 146 | The supported way: 147 | 148 | ```console 149 | $ goeval 'var fact func(int)int;fact=func(n int)int{if n==1{return 1};return n*fact(n-1)};fmt.Println(fact(5))' 150 | ``` 151 | 152 | The hacky way: 153 | 154 | ```console 155 | $ goeval 'fmt.Println(fact(5))};func fact(n int)int{if n==1{return 1};return n*fact(n-1)' 156 | ``` 157 | 158 | ### Use generics 159 | 160 | Needs: 161 | - goeval compiled with Go 1.18+ 162 | - Go 1.18+ installed. 163 | 164 | ```console 165 | $ goeval 'p(1);p("a");};func p[T any](x T){fmt.Println(x)' 166 | 1 167 | a 168 | $ goeval 'p(1);p(2.0);};func p[T int|float64](x T){x++;fmt.Println(x)' 169 | 2 170 | 3 171 | $ goeval -i golang.org/x/exp/constraints 'p(1);p(2.0);};func p[T constraints.Signed|constraints.Float](x T){x++;fmt.Println(x)' 172 | 2 173 | 3 174 | ``` 175 | 176 | ## 🔄 Alternatives 177 | 178 | * [gommand](https://github.com/sno6/gommand) Go one liner program. Similar to `python -c`. 179 | * [gorram](https://github.com/natefinch/gorram) Like `go run` for any Go function. 180 | * [goexec](https://github.com/shurcooL/goexec) A command line tool to execute Go functions. 181 | * [goplay](https://github.com/haya14busa/goplay) Upload code on the Go Playground. `goplay` is like `goeval -share`; `goplay -run` is like `goeval -play`. 182 | 183 | ## 🛡️ License 184 | 185 | Copyright 2019-2025 Olivier Mengué 186 | 187 | Licensed under the Apache License, Version 2.0 (the "License"); 188 | you may not use this file except in compliance with the License. 189 | You may obtain a copy of the License at 190 | 191 | http://www.apache.org/licenses/LICENSE-2.0 192 | 193 | Unless required by applicable law or agreed to in writing, software 194 | distributed under the License is distributed on an "AS IS" BASIS, 195 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 196 | See the License for the specific language governing permissions and 197 | limitations under the License. 198 | -------------------------------------------------------------------------------- /sub/playground-openapi.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # 3 | # Copyright 2025 Olivier Mengué. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | openapi: '3.1.1' 18 | info: 19 | title: Reverse engineered API of the Go Playground 20 | description: > 21 | Server source code: https://go.googlesource.com/playground 22 | https://pkg.go.dev/golang.org/x/playground 23 | 24 | Author of this OpenAPI specification: [Olivier Mengué](https://github.com/dolmen-go). 25 | contact: 26 | url: https://github.com/dolmen-go/goeval/issues 27 | version: '0.20250419.0' 28 | # The license applies only to this document, not to the Go Playground. 29 | license: 30 | name: Apache License 2.0 31 | identifier: Apache-2.0 32 | # identifier and url are exclusive 33 | # url: https://www.apache.org/licenses/LICENSE-2.0 34 | servers: 35 | # The main api is at play.golang.org 36 | # go.dev/_ exposes only some endpoints with a different API (for example /compile has a different schema) 37 | - url: https://play.golang.org 38 | description: play.golang.org is also the backend of https://go.dev/_ which exposes a subset of this API and some variations in output. 39 | paths: 40 | # See https://go.googlesource.com/playground/+/refs/heads/master/server.go#51 41 | /version: 42 | get: # also: post 43 | operationId: version 44 | summary: Show server's Go version. 45 | description: > 46 | Server source code: https://go.googlesource.com/playground/+/refs/heads/master/fmt.go#23 47 | security: [{}] 48 | responses: 49 | '200': 50 | description: OK 51 | headers: 52 | Access-Control-Allow-Origin: 53 | schema: 54 | type: string 55 | enum: ["*"] 56 | content: 57 | "application/json": 58 | schema: 59 | type: object 60 | properties: 61 | Version: 62 | type: string 63 | Release: 64 | type: string 65 | Name: 66 | type: string 67 | example: {"Version":"go1.24.2","Release":"go1.24","Name":"Go 1.24"} 68 | /compile: 69 | post: 70 | operationId: play 71 | summary: Compile and run the provided Go code. 72 | description: > 73 | Curl example: 74 | ```console 75 | $ curl -s -X POST --data-urlencode body@- https://play.golang.org/compile < 143 | See https://go.googlesource.com/playground/+/refs/heads/master/play.go#62 144 | properties: 145 | Delay: 146 | type: string 147 | description: Go's [time.Duration](https://pkg.go.dev/time#ParseDuration). 148 | Kind: 149 | type: string 150 | enum: 151 | - stdout 152 | - stderr 153 | Message: 154 | type: string 155 | '500': 156 | description: Internal server error. 157 | headers: 158 | Access-Control-Allow-Origin: 159 | schema: 160 | type: string 161 | enum: ["*"] 162 | /share: 163 | # https://go.googlesource.com/playground/+/refs/heads/master/share.go 164 | post: 165 | operationId: share 166 | summary: Save and share the code. 167 | description: > 168 | Curl example: 169 | ```console 170 | $ curl --data-binary '@-' -H 'Content-Type: text/plain; charset=utf-8' 'https://play.golang.org/_/share' < 222 | Curl examples: 223 | ```console 224 | $ curl -s --data-urlencode body@- https://play.golang.org/fmt < 284 | Server source code: https://go.googlesource.com/playground/+/refs/heads/master/edit.go 285 | parameters: 286 | - name: id 287 | description: Snippet identifier, as returned by `/share`. 288 | in: path 289 | schema: 290 | type: string 291 | required: true 292 | - name: download 293 | description: If `true`, set the Content-Disposition header. 294 | in: query # https://pkg.go.dev/net/http#Request.FormValue 295 | schema: 296 | type: boolean 297 | security: [{}] 298 | responses: 299 | '200': 300 | description: Snippet content. 301 | headers: 302 | Access-Control-Allow-Origin: 303 | schema: 304 | type: string 305 | enum: ["*"] 306 | content: 307 | 'text/plain; charset=utf-8': 308 | schema: 309 | type: string 310 | '404': 311 | description: Snippet not found. 312 | headers: 313 | Access-Control-Allow-Origin: 314 | schema: 315 | type: string 316 | enum: ["*"] 317 | content: 318 | 'text/plain': 319 | schema: 320 | type: string 321 | enum: ["Snippet not found"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2025 Olivier Mengué. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "bytes" 21 | "errors" 22 | "flag" 23 | "fmt" 24 | "io" 25 | "log" 26 | "os" 27 | "os/exec" 28 | "path/filepath" 29 | "runtime" 30 | "strings" 31 | "time" 32 | 33 | "golang.org/x/mod/module" 34 | goimp "golang.org/x/tools/imports" 35 | ) 36 | 37 | // imports is the storage for -i flags 38 | // imports implements interface flag.Value. 39 | type imports struct { 40 | packages map[string]string // alias => import path 41 | modules map[string]string // module path => version 42 | onlySemVer bool 43 | } 44 | 45 | func (*imports) String() string { 46 | return "" // irrelevant 47 | } 48 | 49 | func (imp *imports) Set(s string) error { 50 | // Allow -i fmt,os 51 | // Comma is not allowed in import path 52 | if p1, remainder, ok := strings.Cut(s, ","); ok { 53 | err := imp.Set(p1) 54 | if err == nil { 55 | imp.Set(remainder) 56 | } 57 | return err 58 | } 59 | 60 | // Optional aliasing with [alias=]import 61 | var alias, path, version string 62 | var ok bool 63 | if alias, path, ok = strings.Cut(s, "="); !ok { 64 | alias = "" 65 | path = s 66 | } else if alias == "" { 67 | return fmt.Errorf("%q: empty alias", s) 68 | } else if alias == "_" || alias == "." { 69 | tmpPath, _, _ := strings.Cut(path, "@") 70 | alias = alias + " " + tmpPath // special alias 71 | } else if strings.Contains(alias, " ") { 72 | return fmt.Errorf("%q: invalid alias", s) 73 | } 74 | var p2 string 75 | if p2, version, ok = strings.Cut(path, "@"); ok { 76 | if version == "" { 77 | return fmt.Errorf("%q: empty module version", s) 78 | } 79 | path = p2 80 | if err := module.CheckPath(path); err != nil { 81 | return fmt.Errorf("%q: %w", s, err) 82 | } 83 | // TODO check for duplicates 84 | if imp.modules == nil { 85 | imp.modules = make(map[string]string) 86 | } 87 | imp.modules[path] = version 88 | imp.onlySemVer = imp.onlySemVer && version == module.CanonicalVersion(version) 89 | } else if alias == "" { 90 | alias = " " + path // special alias 91 | } 92 | 93 | switch path { 94 | case "": 95 | return fmt.Errorf("%q: empty path", s) 96 | case "embed": 97 | return errors.New("use of package 'embed' is not allowed") 98 | 99 | default: 100 | if err := module.CheckImportPath(path); err != nil { 101 | return fmt.Errorf("%q: %w", s, err) 102 | } 103 | } 104 | 105 | if alias != "" { 106 | imp.packages[alias] = path 107 | } 108 | 109 | // log.Printf("alias=%s path=%s version=%s", alias, path, version) 110 | 111 | return nil 112 | } 113 | 114 | // Reference code for running the "go" command: 115 | // https://github.com/golang/dl/blob/master/internal/version/version.go#L58 116 | 117 | var run = runSilent 118 | 119 | func runSilent(cmd *exec.Cmd) error { 120 | return cmd.Run() 121 | } 122 | 123 | func runX(cmd *exec.Cmd) error { 124 | // Inject -x in go commands 125 | if cmd.Args[0] == goCmd && cmd.Args[1] != "env" { 126 | cmd.Args = append([]string{goCmd, cmd.Args[1], "-x"}, cmd.Args[2:]...) 127 | } 128 | fmt.Printf("%s\n", cmd.Args) 129 | return cmd.Run() 130 | } 131 | 132 | func runTime(cmd *exec.Cmd) error { 133 | defer func(start time.Time) { 134 | fmt.Fprintf(os.Stderr, "run %v %v\n", time.Since(start), cmd.Args) 135 | }(time.Now()) 136 | return cmd.Run() 137 | } 138 | 139 | func gorun(srcFilename string, env []string, buildDir string, runDir string, args ...string) error { 140 | exePath := buildOutput 141 | if exePath == "" { 142 | 143 | exeDir, err := os.MkdirTemp("", "goeval*") 144 | if err != nil { 145 | return err 146 | } 147 | 148 | defer func() { 149 | if err := os.RemoveAll(exeDir); err != nil { 150 | log.Printf("RemoveAll(%q): %v", exeDir, err) 151 | } 152 | }() 153 | 154 | exePath = filepath.Join(exeDir, "goeval-run") 155 | if runtime.GOOS == "windows" { 156 | exePath += ".exe" 157 | } 158 | } 159 | 160 | cmdBuild := exec.Command(goCmd, "build", 161 | // Do not embed VCS info: 162 | // - there is nothing if fully built from temp dir (module mode) 163 | // - or, if present, is not relevant for quick exec (GOPATH mode) 164 | "-buildvcs=false", 165 | // Trim paths because the paths of our ephemeral source files will not be helpful in a stack trace. 166 | // This also hides goeval implementation details. 167 | "-trimpath", 168 | 169 | "-o", exePath, 170 | srcFilename) 171 | cmdBuild.Env = env 172 | cmdBuild.Dir = buildDir 173 | cmdBuild.Stdout = os.Stdout 174 | cmdBuild.Stderr = os.Stderr 175 | if err := run(cmdBuild); err != nil { 176 | return fmt.Errorf("failed to build: %w", err) 177 | } 178 | 179 | // actionBuild: don't run 180 | if buildOutput != "" { 181 | return nil 182 | } 183 | 184 | cmdRun := exec.Command(exePath, args...) 185 | cmdRun.Env = env 186 | cmdRun.Dir = runDir // In Go module mode we run from the temp module dir 187 | cmdRun.Stdin = os.Stdin 188 | cmdRun.Stdout = os.Stdout 189 | cmdRun.Stderr = os.Stderr 190 | return run(cmdRun) 191 | } 192 | 193 | var goCmd = "go" 194 | 195 | func getGOMODCACHE(env []string) (string, error) { 196 | var out bytes.Buffer 197 | cmd := exec.Command(goCmd, "env", "GOMODCACHE") 198 | cmd.Stderr = os.Stderr 199 | cmd.Stdout = &out 200 | cmd.Env = env 201 | err := run(cmd) 202 | if err != nil { 203 | return "", err 204 | } 205 | b := bytes.TrimRight(out.Bytes(), "\r\n") 206 | if len(b) == 0 { 207 | return "", errors.New("can't retrieve GOMODCACHE") 208 | } 209 | return string(b), nil 210 | } 211 | 212 | func main() { 213 | err := _main() 214 | if exit, ok := err.(*exec.ExitError); ok && exit.ExitCode() > 0 { 215 | os.Exit(exit.ExitCode()) 216 | } else if err != nil { 217 | log.Fatal(err) 218 | } 219 | } 220 | 221 | type actionBits uint 222 | 223 | const ( 224 | actionRun actionBits = iota 225 | actionBuild // -o ... 226 | actionDump // -E 227 | actionDumpPlay // -Eplay 228 | actionPlay // -play 229 | actionShare // -share 230 | 231 | actionDefault = actionRun 232 | ) 233 | 234 | var ( 235 | action actionBits 236 | buildOutput string // -o 237 | 238 | errActionExclusive = errors.New("flags -o, -E, -Eplay, -play and -share are exclusive") 239 | ) 240 | 241 | func flagAction(name string, a actionBits, target *string, usage string) { 242 | flag.BoolFunc(name, usage, func(value string) error { 243 | if target == nil && value != "true" { 244 | return errors.New("no value expected") 245 | } 246 | if action != actionDefault { 247 | return errActionExclusive 248 | } 249 | action = a 250 | return nil 251 | }) 252 | } 253 | 254 | func _main() error { 255 | imports := imports{ 256 | packages: map[string]string{}, 257 | onlySemVer: true, 258 | } 259 | flag.Var(&imports, "i", ``+ 260 | "* import package local package from GOPATH: [alias=]import-path\n"+ 261 | "* import package in Go module mode: [alias=]import-path@version\n"+ 262 | "Once a version is mentioned, Go module mode is enabled globally.", 263 | ) 264 | 265 | var goimports string 266 | flag.StringVar(&goimports, "goimports", "goimports", "goimports tool name, to use an alternate tool or just disable it.") 267 | 268 | flag.StringVar(&goCmd, "go", "go", "go command path.") 269 | 270 | // -E, like "cc -E" 271 | flagAction("E", actionDump, nil, "just dump the assembled source, without running it.") 272 | flagAction("Eplay", actionDumpPlay, nil, "just dump the assembled source for posting on https://go.dev/play") 273 | 274 | // -play, -share 275 | registerOnlineFlags() 276 | 277 | flag.Func("o", "just build a binary, don't execute.", func(value string) (err error) { 278 | if action != actionDefault { 279 | return errActionExclusive 280 | } 281 | if value == "" { 282 | return errors.New("invalid empty output file") 283 | } 284 | action = actionBuild 285 | buildOutput, err = filepath.Abs(value) 286 | return 287 | }) 288 | 289 | showCmds := flag.Bool("x", false, "print commands executed.") 290 | 291 | flag.Usage = func() { 292 | prog := os.Args[0] 293 | fmt.Fprintf(flag.CommandLine.Output(), ""+ 294 | "\n"+ 295 | "Usage: %s [...] [...]\n"+ 296 | "\n"+ 297 | "Options:\n", 298 | prog) 299 | flag.PrintDefaults() 300 | fmt.Fprintf(flag.CommandLine.Output(), ""+ 301 | "\n"+ 302 | "Example:\n"+ 303 | " %s -i fmt 'fmt.Println(\"Hello, world!\")'\n"+ 304 | "\n"+ 305 | "Copyright 2019-2025 Olivier Mengué.\n"+ 306 | "Source code: https://github.com/dolmen-go/goeval\n", 307 | prog) 308 | os.Exit(1) 309 | } 310 | flag.Parse() 311 | 312 | if flag.NArg() < 1 { 313 | flag.Usage() 314 | } 315 | code := flag.Arg(0) 316 | if code == "-" { 317 | b, err := io.ReadAll(os.Stdin) 318 | if err != nil { 319 | return err 320 | } 321 | code = string(b) 322 | } 323 | 324 | args := flag.Args()[1:] 325 | if len(args) > 0 { 326 | switch action { 327 | case actionBuild, actionDump: 328 | return errors.New("arguments not expected") 329 | } 330 | } 331 | 332 | if goCmdResolved, err := exec.LookPath(goCmd); err != nil { 333 | return fmt.Errorf("%q: %v", goCmd, err) 334 | } else { 335 | goCmd = goCmdResolved 336 | } 337 | 338 | if *showCmds { 339 | run = runX 340 | } 341 | 342 | moduleMode := imports.modules != nil 343 | 344 | env := os.Environ() 345 | if moduleMode { 346 | env = append(env, "GO111MODULE=on") 347 | } else { 348 | // Run in GOPATH mode, ignoring any code in the current directory 349 | env = append(env, "GO111MODULE=off") 350 | } 351 | 352 | var dir, origDir string 353 | 354 | if moduleMode { 355 | // "go get" is not yet as smart as we want, so let's help 356 | // https://go.dev/issue/43646 357 | preferCache := imports.onlySemVer 358 | var gomodcache string 359 | if preferCache { 360 | var err error 361 | gomodcache, err = getGOMODCACHE(env) 362 | preferCache = err == nil 363 | } 364 | 365 | var err error 366 | if dir, err = os.MkdirTemp("", "goeval*"); err != nil { 367 | log.Fatal(err) 368 | } 369 | defer os.Remove(dir) 370 | 371 | moduleName := filepath.Base(dir) 372 | 373 | origDir, err = os.Getwd() 374 | if err != nil { 375 | log.Fatal("getwd:", err) 376 | } 377 | 378 | gomod := dir + "/go.mod" 379 | if err := os.WriteFile(gomod, []byte("module "+moduleName+"\n"), 0600); err != nil { 380 | log.Fatal("go.mod:", err) 381 | } 382 | defer os.Remove(gomod) 383 | 384 | var gogetArgs []string 385 | gogetArgs = append(gogetArgs, "get", "--") 386 | for mod, ver := range imports.modules { 387 | gogetArgs = append(gogetArgs, mod+"@"+ver) 388 | if preferCache { 389 | // Keep preferCache as long as we find modules in the cache. 390 | // Structure of the cache is documented here: https://go.dev/ref/mod#module-cache 391 | _, err := os.Stat(gomodcache + "/cache/download/" + mod + "/@v/" + ver + ".mod") 392 | preferCache = err == nil 393 | } 394 | } 395 | for _, path := range imports.packages { 396 | if _, seen := imports.modules[path]; !seen { 397 | gogetArgs = append(gogetArgs, path) 398 | } 399 | } 400 | 401 | // fmt.Println("preferCache", preferCache) 402 | if preferCache { 403 | // As we found all modules in the cache, tell "go get" and "go run" to not use the proxy. 404 | // See https://go.dev/issue/43646 405 | // env = append(env, "GOPROXY=file://"+filepath.ToSlash(gomodcache)+"/cache/download") 406 | env = append(env, "GOPROXY=off") 407 | } 408 | 409 | // Do not let an inherited GOMOD variable go through. 410 | env = append(env, "GOMOD="+gomod) 411 | 412 | cmd := exec.Command(goCmd, gogetArgs...) 413 | cmd.Env = env 414 | cmd.Dir = dir 415 | cmd.Stdin = nil 416 | cmd.Stdout = nil 417 | cmd.Stdout = os.Stdout 418 | // go get is too verbose :( 419 | cmd.Stderr = nil 420 | if err = run(cmd); err != nil { 421 | log.Fatal("go get failure:", err) 422 | } 423 | // log.Println("go get OK.") 424 | defer os.Remove(dir + "/go.sum") 425 | } 426 | 427 | var ( 428 | src bytes.Buffer 429 | injectArgs bool // inject our arguments into os.Args in the program source 430 | ) 431 | 432 | // If sending to the Go Playground, export GOEXPERIMENT as a comment 433 | if action >= actionDumpPlay { 434 | const alphaNum = "abcdefghijklmnopqrstuvwxyz0123456789" 435 | const alphaNumComma = alphaNum + "," 436 | if exp, ok := os.LookupEnv("GOEXPERIMENT"); ok && 437 | exp != "" && // Not empty 438 | strings.Trim(exp, ",") == exp && // No leading or trailing commas 439 | strings.Trim(exp, alphaNumComma) == "" { // only lower case alpha num and comma 440 | src.WriteString("// GOEXPERIMENT=") 441 | src.WriteString(exp) 442 | src.WriteString("\n\n") 443 | } 444 | 445 | injectArgs = len(args) > 0 446 | if injectArgs { 447 | // We need the os package to patch os.Args 448 | imports.Set("os") 449 | } 450 | } 451 | 452 | src.WriteString("package main\n") 453 | for alias, path := range imports.packages { 454 | if len(alias) > 2 && alias[1] == ' ' { 455 | switch alias[0] { 456 | case '.', '_': 457 | alias = alias[:1] 458 | case ' ': // no alias 459 | fmt.Fprintf(&src, "import %q\n", path) 460 | continue 461 | } 462 | } 463 | fmt.Fprintf(&src, "import %s %q\n", alias, path) 464 | } 465 | if injectArgs { 466 | fmt.Fprintf(&src, "func init() { os.Args = append(os.Args[:1], %#v...) }\n\n", args) 467 | } 468 | src.WriteString("func main() {\n") 469 | if action <= actionDump { 470 | src.WriteString("//line :1\n") 471 | } 472 | src.WriteString(code) 473 | src.WriteString("\n}\n") 474 | 475 | var ( 476 | // srcFinal is the final transformed source after goimports. 477 | // When in module mode AND dumping (-E, -Eplay) or sending to the Playground (-play, -share), 478 | // this is not just the Go code, but a Txtar archive that includes go.mod and go.sum. 479 | srcFinal io.Writer 480 | // srcFilename is the full path to the srcFinal on disk that is needed by goimports to locate go.mod. 481 | srcFilename string 482 | // tail is the action that will process srcFinal. 483 | tail func() error 484 | ) 485 | switch action { 486 | case actionRun, actionBuild: 487 | f, err := os.CreateTemp(dir, "*.go") 488 | if err != nil { 489 | log.Fatal(err) 490 | } 491 | defer f.Close() 492 | defer os.Remove(f.Name()) 493 | srcFinal = f 494 | srcFilename = f.Name() 495 | 496 | tail = func() error { 497 | if err = f.Close(); err != nil { 498 | return err 499 | } 500 | return gorun(srcFilename, env, dir, origDir, args...) 501 | } 502 | case actionPlay: 503 | var cleanup func() 504 | srcFinal, tail, cleanup = prepareSubPlay() 505 | defer cleanup() 506 | case actionShare: 507 | var cleanup func() 508 | srcFinal, tail, cleanup = prepareSubShare() 509 | defer cleanup() 510 | default: // actionDump, actionDumpPlay 511 | srcFinal = os.Stdout 512 | tail = func() error { return nil } 513 | } 514 | 515 | var err error 516 | switch goimports { 517 | case "goimports": 518 | var out []byte 519 | var filename string // filename is used to locate the relevant go.mod 520 | if imports.packages != nil { 521 | filename = srcFilename 522 | } 523 | out, err = goimp.Process(filename, src.Bytes(), &goimp.Options{ 524 | Fragment: false, 525 | AllErrors: false, 526 | Comments: true, 527 | TabIndent: true, 528 | TabWidth: 8, 529 | FormatOnly: false, 530 | }) 531 | if err == nil { 532 | _, err = srcFinal.Write(out) 533 | } 534 | case "": 535 | _, err = srcFinal.Write(src.Bytes()) 536 | default: 537 | cmd := exec.Command(goimports) 538 | cmd.Env = env 539 | cmd.Dir = dir 540 | cmd.Stdin = &src 541 | cmd.Stdout = srcFinal 542 | cmd.Stderr = os.Stderr 543 | err = run(cmd) 544 | } 545 | if err != nil { 546 | return err 547 | } 548 | 549 | /* 550 | // Do we need to run "go get" again after "goimports"? 551 | if moduleMode { 552 | goget := exec.Command(goCmd, "get", ".") 553 | goget.Env = env 554 | goget.Dir = dir 555 | goget.Stdout = os.Stdout 556 | goget.Stderr = os.Stderr 557 | run(goget) 558 | } 559 | */ 560 | 561 | // dump go.mod, go.sum 562 | if moduleMode && action >= actionDump { 563 | gomod, err := os.Open(dir + "/go.mod") 564 | if err != nil { 565 | log.Fatal(err) 566 | } 567 | io.WriteString(srcFinal, "-- go.mod --\n") 568 | defer gomod.Close() 569 | io.Copy(srcFinal, gomod) 570 | 571 | gosum, err := os.Open(dir + "/go.sum") 572 | switch { 573 | case errors.Is(err, os.ErrNotExist): // ignore 574 | case err != nil: 575 | log.Fatal(err) 576 | default: 577 | io.WriteString(srcFinal, "-- go.sum --\n") 578 | defer gosum.Close() 579 | io.Copy(srcFinal, gosum) 580 | } 581 | } 582 | 583 | return tail() 584 | } 585 | --------------------------------------------------------------------------------