├── .gitignore ├── go.mod ├── go.sum ├── .github └── workflows │ └── tests.yml ├── LICENSE.txt ├── README.md ├── sniplib ├── sniplib.go └── sniplib_test.go └── gosnip.go /.gitignore: -------------------------------------------------------------------------------- 1 | gosnip 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/benhoyt/gosnip 2 | 3 | go 1.19 4 | 5 | require golang.org/x/tools v0.7.0 6 | 7 | require ( 8 | golang.org/x/mod v0.9.0 // indirect 9 | golang.org/x/sys v0.6.0 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= 2 | golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 3 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 4 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 5 | golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= 6 | golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= 7 | -------------------------------------------------------------------------------- /.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 | build-linux: 11 | strategy: 12 | matrix: 13 | go: ['1.20', '1.19'] 14 | os: [ubuntu-latest, macos-latest, windows-latest] 15 | 16 | name: Go ${{ matrix.go }} on ${{ matrix.os }} 17 | 18 | runs-on: ${{ matrix.os }} 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | 23 | - name: Set up Go 24 | uses: actions/setup-go@v4 25 | with: 26 | go-version: ${{ matrix.go }} 27 | 28 | - name: Run tests 29 | run: | 30 | go version 31 | go test -race ./... 32 | 33 | - name: Test that binary builds 34 | run: | 35 | go build 36 | ./gosnip "fmt.Println(time.Now())" 37 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gosnip: run small snippets of Go code from the command line 2 | 3 | [![Documentation](https://pkg.go.dev/badge/github.com/benhoyt/gosnip)](https://pkg.go.dev/github.com/benhoyt/gosnip) 4 | [![GitHub Actions Build](https://github.com/benhoyt/gosnip/workflows/Tests/badge.svg)](https://github.com/benhoyt/gosnip/actions/workflows/tests.yml) 5 | 6 | Package gosnip is a tool that allows you to run small snippets of 7 | Go code from the command line. 8 | 9 | usage: gosnip [-d] [-i import ...] statements... 10 | 11 | To download and install, use `go install`: 12 | 13 | $ go install github.com/benhoyt/gosnip@latest 14 | 15 | For simple uses, just specify one or more Go statements on the 16 | command line, and gosnip will roll them into a full Go program and 17 | run the result using "go run". Standard library imports and any 18 | imports needed for packages in GOPATH are added automatically 19 | (using the same logic as the "goimports" tool). Some examples: 20 | 21 | $ gosnip 'fmt.Println("Hello world")' 22 | Hello world 23 | 24 | $ gosnip 'fmt.Println("Current time:")' 'fmt.Println(time.Now())' 25 | Current time: 26 | 2018-11-24 16:18:47.101951 -0500 EST m=+0.000419239 27 | 28 | The -i flag allows you to specify an import explicitly, which may be 29 | needed to select between ambiguous stdlib imports such as 30 | "text/template" and "html/template" (multiple -i flags are 31 | allowed). For example: 32 | 33 | $ gosnip -i text/template 't, _ := template.New("w").Parse("{{ . }}\n")' \ 34 | 't.Execute(os.Stdout, "")' 35 | 36 | $ gosnip -i html/template 't, _ := template.New("w").Parse("{{ . }}\n")' \ 37 | 't.Execute(os.Stdout, "")' 38 | <b> 39 | 40 | The -d flag turns on debug mode, which prints the full program on 41 | stderr before running it. For example: 42 | 43 | $ gosnip -d 'fmt.Println(time.Now())' 44 | package main 45 | 46 | import ( 47 | "fmt" 48 | "time" 49 | ) 50 | 51 | func main() { 52 | fmt.Println(time.Now()) 53 | } 54 | 2018-11-24 16:33:56.681024 -0500 EST m=+0.000383308 55 | 56 | The gosnip command-line tool is a thin wrapper around the 57 | "sniplib" package. To run Go snippets in your Go programs, see the 58 | [sniplib docs](https://godoc.org/github.com/benhoyt/gosnip/sniplib). 59 | 60 | ## Why? 61 | 62 | I made gosnip because when coding in Go I often want to try little 63 | snippets of code to see what they do, for example, "how does format 64 | string `%6.3f` work again?" I could use the 65 | [Go playground](https://play.golang.org/), but it's nice to be able 66 | to use a one-line command. Also, I often develop while offline on my 67 | bus commute, so don't have access to the online Go playground (yes, I 68 | know it's possible to run the Go playground locally). 69 | 70 | ## License 71 | 72 | gosnip is licensed under an open source [MIT license](https://github.com/benhoyt/gosnip/blob/master/LICENSE.txt). 73 | 74 | ## Contact me 75 | 76 | Have fun, and please [contact me](https://benhoyt.com/) if you're using gosnip or have any feedback! 77 | -------------------------------------------------------------------------------- /sniplib/sniplib.go: -------------------------------------------------------------------------------- 1 | // Package sniplib converts Go code snippets to full programs and 2 | // runs them. 3 | package sniplib 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "os" 11 | "os/exec" 12 | "strings" 13 | 14 | importspkg "golang.org/x/tools/imports" 15 | ) 16 | 17 | // ToProgram converts a slice of Go statements to a full Go program. 18 | // References to standard library functions and functions from 19 | // packages in GOPATH are imported automatically (using the same 20 | // logic as the "goimports" tool). 21 | // 22 | // The "imports" arg is an explicit list of imports, only needed for 23 | // selecting between ambiguous stdlib import names like 24 | // "text/template" and "html/template". Returns the formatted source 25 | // text of the full Go program and any error that occurred. 26 | func ToProgram(statements, imports []string) (string, error) { 27 | importStrs := make([]string, len(imports)) 28 | for i, imp := range imports { 29 | importStrs[i] = fmt.Sprintf("import %q", imp) 30 | } 31 | source := fmt.Sprintf(` 32 | package main 33 | 34 | %s 35 | 36 | func main() { 37 | %s 38 | }`, strings.Join(importStrs, "\n"), strings.Join(statements, "; ")) 39 | processed, err := importspkg.Process("", []byte(source), nil) 40 | if err != nil { 41 | return "", err 42 | } 43 | return string(processed), nil 44 | } 45 | 46 | // Run runs the given Go program source. Use the provided stdin 47 | // reader and stdout/stderr writers for I/O. 48 | func Run(source string, stdin io.Reader, stdout, stderr io.Writer) error { 49 | // First write source to a temporary file (deleted afterwards) 50 | file, err := ioutil.TempFile("", "gosnip_*.go") 51 | if err != nil { 52 | return err 53 | } 54 | defer os.Remove(file.Name()) 55 | _, err = io.WriteString(file, source) 56 | if err != nil { 57 | return err 58 | } 59 | err = file.Close() 60 | if err != nil { 61 | return err 62 | } 63 | 64 | // Then use "go run" to run it 65 | cmd := exec.Command("go", "run", file.Name()) 66 | cmd.Stdin = stdin 67 | cmd.Stdout = stdout 68 | errBuf := &bytes.Buffer{} 69 | cmd.Stderr = errBuf 70 | err = cmd.Run() 71 | if err != nil { 72 | // Ideally we'd only do this filtering on compile error, not program 73 | // error, but it's hard to tell the difference ("go run" used to 74 | // return exit code 2 for this, but since Go 1.20, it doesn't). 75 | filterStderr(errBuf.Bytes(), stderr) 76 | return err 77 | } 78 | _, _ = errBuf.WriteTo(stderr) 79 | return err 80 | } 81 | 82 | // Filter out extraneous output and temp file name from "go run" 83 | // output in case of compile error. Example "go run" output: 84 | // 85 | // # command-line-arguments 86 | // /var/folders/sz/thh6m7316b3gvvvmjp8qpdrm0000gp/T/gosnip_615300750.go:8:2: undefined: fmt.X 87 | // 88 | // Output after filtering: 89 | // 90 | // 8:2: undefined: fmt.X 91 | func filterStderr(data []byte, writer io.Writer) { 92 | if !bytes.Contains(data, []byte("# command-line-arguments")) { 93 | writer.Write(data) 94 | return 95 | } 96 | lines := bytes.Split(data, []byte("\n")) 97 | for _, line := range lines { 98 | if bytes.HasPrefix(line, []byte("# ")) { 99 | continue 100 | } 101 | pos := bytes.Index(line, []byte(".go:")) 102 | if pos < 0 { 103 | continue 104 | } 105 | writer.Write(line[pos+4:]) 106 | writer.Write([]byte("\n")) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /gosnip.go: -------------------------------------------------------------------------------- 1 | // Package gosnip is a tool that allows you to run small snippets of 2 | // Go code from the command line. 3 | // 4 | // usage: gosnip [-d] [-i import ...] statements... 5 | // 6 | // For simple uses, just specify one or more Go statements on the 7 | // command line, and gosnip will roll them into a full Go program and 8 | // run the result using "go run". Standard library imports and any 9 | // imports needed for packages in GOPATH are added automatically 10 | // (using the same logic as the "goimports" tool). Some examples: 11 | // 12 | // $ gosnip 'fmt.Println("Hello world")' 13 | // Hello world 14 | // 15 | // $ gosnip 'fmt.Println("Current time:")' 'fmt.Println(time.Now())' 16 | // Current time: 17 | // 2018-11-24 16:18:47.101951 -0500 EST m=+0.000419239 18 | // 19 | // The -i flag allows you to specify an import explicitly, which may be 20 | // needed to select between ambiguous stdlib imports such as 21 | // "text/template" and "html/template" (multiple -i flags are 22 | // allowed). For example: 23 | // 24 | // $ ./gosnip -i text/template 't, _ := template.New("w").Parse("{{ . }}\n")' \ 25 | // 't.Execute(os.Stdout, "")' 26 | // 27 | // $ ./gosnip -i html/template 't, _ := template.New("w").Parse("{{ . }}\n")' \ 28 | // 't.Execute(os.Stdout, "")' 29 | // <b> 30 | // 31 | // The -d flag turns on debug mode, which prints the full program on 32 | // stderr before running it. For example: 33 | // 34 | // $ gosnip -d 'fmt.Println(time.Now())' 35 | // package main 36 | // 37 | // import ( 38 | // "fmt" 39 | // "time" 40 | // ) 41 | // 42 | // func main() { 43 | // fmt.Println(time.Now()) 44 | // } 45 | // 2018-11-24 16:33:56.681024 -0500 EST m=+0.000383308 46 | // 47 | // The gosnip command-line tool is a thin wrapper around the 48 | // "sniplib" package. To run Go snippets in your Go programs, see the 49 | // sniplib docs. 50 | package main 51 | 52 | import ( 53 | "flag" 54 | "fmt" 55 | "os" 56 | 57 | "github.com/benhoyt/gosnip/sniplib" 58 | ) 59 | 60 | const ( 61 | version = "v1.2.0" 62 | ) 63 | 64 | func main() { 65 | var imports multiString 66 | flag.Var(&imports, "i", "import `package` explicitly; multiple -i flags allowed\n(usually used for non-stdlib packages)") 67 | debug := flag.Bool("d", false, "debug mode (print full source to stderr)") 68 | showVersion := flag.Bool("version", false, "show gosnip version and exit") 69 | flag.Parse() 70 | args := flag.Args() 71 | 72 | if *showVersion { 73 | fmt.Printf("gosnip %s - Copyright (c) 2018 Ben Hoyt\n", version) 74 | return 75 | } 76 | 77 | if len(args) < 1 { 78 | errorExit("usage: gosnip [-d] [-i import ...] statements...\n") 79 | } 80 | 81 | source, err := sniplib.ToProgram(args, imports) 82 | if err != nil { 83 | errorExit("%v\n", err) 84 | } 85 | if *debug { 86 | fmt.Fprint(os.Stderr, source) 87 | } 88 | 89 | err = sniplib.Run(source, os.Stdin, os.Stdout, os.Stderr) 90 | if err != nil { 91 | os.Exit(1) 92 | } 93 | } 94 | 95 | func errorExit(format string, args ...interface{}) { 96 | fmt.Fprintf(os.Stderr, format, args...) 97 | os.Exit(1) 98 | } 99 | 100 | type multiString []string 101 | 102 | func (m *multiString) String() string { 103 | return fmt.Sprintf("%v", []string(*m)) 104 | } 105 | 106 | func (m *multiString) Set(value string) error { 107 | *m = append(*m, value) 108 | return nil 109 | } 110 | -------------------------------------------------------------------------------- /sniplib/sniplib_test.go: -------------------------------------------------------------------------------- 1 | // Tests and examples for gosnip's sniplib package 2 | 3 | package sniplib_test 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "os" 9 | "regexp" 10 | "strings" 11 | "testing" 12 | 13 | "github.com/benhoyt/gosnip/sniplib" 14 | ) 15 | 16 | func TestToProgram(t *testing.T) { 17 | tests := []struct { 18 | statements []string 19 | imports []string 20 | output string 21 | }{ 22 | { 23 | []string{`fmt.Println("Hello world")`}, 24 | nil, 25 | `package main 26 | 27 | import "fmt" 28 | 29 | func main() { 30 | fmt.Println("Hello world") 31 | } 32 | `}, 33 | { 34 | []string{`fmt.Println(time.Now())`}, 35 | []string{}, 36 | `package main 37 | 38 | import ( 39 | "fmt" 40 | "time" 41 | ) 42 | 43 | func main() { 44 | fmt.Println(time.Now()) 45 | } 46 | `}, 47 | { 48 | []string{`fmt.Println("x"); fmt.Println(int(3.5))`}, 49 | []string{}, 50 | `package main 51 | 52 | import "fmt" 53 | 54 | func main() { 55 | fmt.Println("x") 56 | fmt.Println(int(3.5)) 57 | } 58 | `}, 59 | { 60 | []string{`template.Must()`}, 61 | []string{"text/template"}, 62 | `package main 63 | 64 | import "text/template" 65 | 66 | func main() { 67 | template.Must() 68 | } 69 | `}, 70 | { 71 | []string{`foo.Bar()`}, 72 | []string{"github.com/user/foo"}, 73 | `package main 74 | 75 | import "github.com/user/foo" 76 | 77 | func main() { 78 | foo.Bar() 79 | } 80 | `}, 81 | { 82 | []string{`fmt.Println(rand.Intn)`}, // don't call it (it's not very testable) 83 | nil, 84 | `package main 85 | 86 | import ( 87 | "fmt" 88 | "math/rand" 89 | ) 90 | 91 | func main() { 92 | fmt.Println(rand.Intn) 93 | } 94 | `}, 95 | { 96 | []string{`fmt.Println(`}, 97 | []string{}, 98 | "ERROR: 8:1: expected operand, found '}'", 99 | }, 100 | } 101 | for _, test := range tests { 102 | name := strings.Join(test.statements, "; ") 103 | t.Run(name, func(t *testing.T) { 104 | source, err := sniplib.ToProgram(test.statements, test.imports) 105 | if err != nil { 106 | errStr := "ERROR: " + err.Error() 107 | if errStr != test.output { 108 | t.Fatalf("expected %q, got %q", test.output, errStr) 109 | } 110 | } else { 111 | if source != test.output { 112 | t.Fatalf("expected:\n%sgot:\n%s", test.output, source) 113 | } 114 | } 115 | }) 116 | } 117 | } 118 | 119 | func TestRun(t *testing.T) { 120 | tests := []struct { 121 | source string 122 | stdout string 123 | stderr string 124 | err string 125 | }{ 126 | { 127 | `package main 128 | 129 | import ( 130 | "fmt" 131 | ) 132 | 133 | func main() { 134 | fmt.Println("Hello world") 135 | } 136 | `, 137 | "Hello world\n", 138 | "", 139 | "", 140 | }, 141 | { 142 | `package main 143 | 144 | import ( 145 | "fmt" 146 | ) 147 | 148 | func main() { 149 | fmt.X() 150 | } 151 | `, 152 | "", 153 | "8:6: undefined: fmt.X\n", 154 | "exit status [12]", 155 | }, 156 | { 157 | `package main 158 | 159 | import ( 160 | "fmt" 161 | "os" 162 | ) 163 | 164 | func main() { 165 | fmt.Fprintf(os.Stderr, "a funky error\n") 166 | os.Exit(5) 167 | } 168 | `, 169 | "", 170 | "a funky error\nexit status 5\n", 171 | "exit status 1", 172 | }, 173 | { 174 | `package main 175 | 176 | func main() { 177 | foo.Bar() 178 | } 179 | `, 180 | "", 181 | "4:2: undefined: foo\n", 182 | "exit status [12]", 183 | }, 184 | } 185 | for _, test := range tests { 186 | t.Run(test.source, func(t *testing.T) { 187 | inBuf := &bytes.Buffer{} 188 | outBuf := &bytes.Buffer{} 189 | errBuf := &bytes.Buffer{} 190 | err := sniplib.Run(test.source, inBuf, outBuf, errBuf) 191 | if outBuf.String() != test.stdout { 192 | t.Errorf("expected stdout %q, got %q", test.stdout, outBuf.String()) 193 | } 194 | if errBuf.String() != test.stderr { 195 | t.Errorf("expected stderr %q, got %q", test.stderr, errBuf.String()) 196 | } 197 | if err != nil { 198 | if !mustMatch(test.err, err.Error()) { 199 | t.Errorf("expected error to match %q, got %q", test.err, err.Error()) 200 | } 201 | } else { 202 | if test.err != "" { 203 | t.Errorf("expected error to match %q, got no error", test.err) 204 | } 205 | } 206 | }) 207 | } 208 | } 209 | 210 | func mustMatch(pattern, s string) bool { 211 | matched, err := regexp.MatchString(pattern, s) 212 | if err != nil { 213 | panic(err) 214 | } 215 | return matched 216 | } 217 | 218 | func ExampleToProgram() { 219 | statements := []string{`fmt.Println("Hello world")`} 220 | source, err := sniplib.ToProgram(statements, nil) 221 | if err != nil { 222 | fmt.Println(err) 223 | return 224 | } 225 | fmt.Print(source) 226 | // Output: 227 | // package main 228 | // 229 | // import "fmt" 230 | // 231 | // func main() { 232 | // fmt.Println("Hello world") 233 | // } 234 | } 235 | 236 | func ExampleToProgram_imports() { 237 | statements := []string{`fmt.Println(template.HTMLEscapeString(""))`} 238 | imports := []string{"text/template"} 239 | source, err := sniplib.ToProgram(statements, imports) 240 | if err != nil { 241 | fmt.Println(err) 242 | return 243 | } 244 | fmt.Print(source) 245 | // Output: 246 | // package main 247 | // 248 | // import ( 249 | // "fmt" 250 | // "text/template" 251 | // ) 252 | // 253 | // func main() { 254 | // fmt.Println(template.HTMLEscapeString("")) 255 | // } 256 | } 257 | 258 | func ExampleRun() { 259 | source := ` 260 | package main 261 | 262 | import "fmt" 263 | 264 | func main() { 265 | fmt.Println("Hello world") 266 | } 267 | ` 268 | err := sniplib.Run(source, os.Stdin, os.Stdout, os.Stderr) 269 | if err != nil { 270 | fmt.Println(err) 271 | return 272 | } 273 | // Output: 274 | // Hello world 275 | } 276 | --------------------------------------------------------------------------------