Goscript: Go For (go-pher?) Scripting

Scripts are short bits of code typically written to automate a series of otherwise manual steps or accomplish a small task. With that focus on quickly getting some task done, a compiled, strongly-typed, multi-threaded language might not be your first thought, but Go has a lot going for it when it comes to scripting. With a bit of support, it can be a compelling choice. Using Go for scripting isn't a new idea. It's attractive because Go has a fantastic standard library and a large ecosystem of third-party packages, the compiler is very fast and a tool like go run almost makes it feel like an interpreted scripting language. And, as a natively compiled language, Go is fast. Some say Go is too verbose for scripting, but I'll take "easy to read and understand" over "cryptic but powerful" every time. Goscript is an opinionated tool aimed at making scripting in Go more convenient. It was inspired by bitfield/script, which brings functionality similar to Unix pipes to Go. By opinionated, I mean that Goscript embraces Go modules (rather than GOPATH with modules turned off) and uses a dedicated module project for your "scripts". It asks you to update your PATH as part of the setup so that your scripts can be immediately available as system-wide commands. It enables treating Go code like a local script with --exec and shebang options, but encourages reuse by letting you name your commands and then saving them, together with source, in your dedicated Goscript project. Finally, its many options are designed to make the project fade into the background and make writing scripts in Go convenient and easy like your favorite scripting language. Goscript Features: Execute simple go code directly on the command line with automatic imports for stdlib and your most-used third-party packages Execute Go source files directly with the --exec option or by including the Unix shebang Compile Go code into named binaries for repeated use, enabling you to build up a library of custom commands Automatically "go get" missing modules required by imports (and remove them when the code importing them is deleted) Organize all of your Go "scripts" in one project, accessible system-wide Generate a template source file for your next Go script List, edit and export sources and binaries for existing commands ... Of course, implied in the term "script" is the idea that the code is relatively small and uncomplicated. If you need multiple source files or a complex build process, you will want to create a dedicated Go project. Installation Do go install github.com/fkmiec/goscript@latest followed by goscript --setup help and follow the instructions. How It Works The goscript executable will wrap any code specified on the command line with a main function and apply any specified imports before compiling and optionally executing the code. By default the binary will be [project folder]/bin/gocmd and the source file will be [project folder]/src/gocmd.go. If a name is given, the binary and source files will reflect that name. By adding the [project] and [project]/bin folders to your PATH environment variable, the goscript command can be used anywhere on your system and the resulting binaries you build with it will also be immediately available to execute like other system commands (such as ls, cat, echo, grep, etc.). If the --file option is used, then Goscript will assume the file is a complete Go source file and build it as is, automatically downloading missing modules required by imports. The --exec option can be included to run it immediately. On Linux and Mac, a shebang at the top of a Go source file works similarly. The --template option can print out a skeleton source file as a starting point, including a shebang line at the top in case your aim is to produce a locally executable script. While the shebang is incompatible with Go, Goscript automatically strips it out before compiling. Nearly every other Goscript option is intended to let you work with Go code where you are and put the project and modules back of mind. Let's See Some Examples We'll use toy examples to keep things simple and demonstrate some features of Goscript. An inaugural "Hello World!" can be executed directly on the command line like this: > $ goscript --exec --code 'fmt.Println("Hello Goscript!")' Hello Goscript! Similar to go run, Goscript produced a binary and immediately executed it. Unlike go run, however, Goscript saved the binary (and source) to the project, which you can see with the --list option. > $ goscript --list gocmd By default the binary is given the generic moniker "gocmd". Using --cat, we can see that the code was wrapped in a main function and the fmt package was imported for us. Using the --code option, Goscript will automatically add imports for packages in the standard library and any packages listed in the project's impor

Apr 15, 2025 - 17:45
 0
Goscript: Go For (go-pher?) Scripting

Scripts are short bits of code typically written to automate a series of otherwise manual steps or accomplish a small task. With that focus on quickly getting some task done, a compiled, strongly-typed, multi-threaded language might not be your first thought, but Go has a lot going for it when it comes to scripting. With a bit of support, it can be a compelling choice.

Using Go for scripting isn't a new idea. It's attractive because Go has a fantastic standard library and a large ecosystem of third-party packages, the compiler is very fast and a tool like go run almost makes it feel like an interpreted scripting language. And, as a natively compiled language, Go is fast. Some say Go is too verbose for scripting, but I'll take "easy to read and understand" over "cryptic but powerful" every time.

Goscript is an opinionated tool aimed at making scripting in Go more convenient. It was inspired by bitfield/script, which brings functionality similar to Unix pipes to Go. By opinionated, I mean that Goscript embraces Go modules (rather than GOPATH with modules turned off) and uses a dedicated module project for your "scripts". It asks you to update your PATH as part of the setup so that your scripts can be immediately available as system-wide commands. It enables treating Go code like a local script with --exec and shebang options, but encourages reuse by letting you name your commands and then saving them, together with source, in your dedicated Goscript project. Finally, its many options are designed to make the project fade into the background and make writing scripts in Go convenient and easy like your favorite scripting language.

Goscript Features:

  • Execute simple go code directly on the command line with automatic imports for stdlib and your most-used third-party packages
  • Execute Go source files directly with the --exec option or by including the Unix shebang
  • Compile Go code into named binaries for repeated use, enabling you to build up a library of custom commands
  • Automatically "go get" missing modules required by imports (and remove them when the code importing them is deleted)
  • Organize all of your Go "scripts" in one project, accessible system-wide
  • Generate a template source file for your next Go script
  • List, edit and export sources and binaries for existing commands
  • ...

Of course, implied in the term "script" is the idea that the code is relatively small and uncomplicated. If you need multiple source files or a complex build process, you will want to create a dedicated Go project.

Installation

Do go install github.com/fkmiec/goscript@latest followed by goscript --setup help and follow the instructions.

How It Works

The goscript executable will wrap any code specified on the command line with a main function and apply any specified imports before compiling and optionally executing the code. By default the binary will be [project folder]/bin/gocmd and the source file will be [project folder]/src/gocmd.go. If a name is given, the binary and source files will reflect that name. By adding the [project] and [project]/bin folders to your PATH environment variable, the goscript command can be used anywhere on your system and the resulting binaries you build with it will also be immediately available to execute like other system commands (such as ls, cat, echo, grep, etc.).

If the --file option is used, then Goscript will assume the file is a complete Go source file and build it as is, automatically downloading missing modules required by imports. The --exec option can be included to run it immediately. On Linux and Mac, a shebang at the top of a Go source file works similarly. The --template option can print out a skeleton source file as a starting point, including a shebang line at the top in case your aim is to produce a locally executable script. While the shebang is incompatible with Go, Goscript automatically strips it out before compiling.

Nearly every other Goscript option is intended to let you work with Go code where you are and put the project and modules back of mind.

Let's See Some Examples

We'll use toy examples to keep things simple and demonstrate some features of Goscript. An inaugural "Hello World!" can be executed directly on the command line like this:

> $ goscript --exec --code 'fmt.Println("Hello Goscript!")'    
Hello Goscript!

Similar to go run, Goscript produced a binary and immediately executed it. Unlike go run, however, Goscript saved the binary (and source) to the project, which you can see with the --list option.

> $ goscript --list                                                       
gocmd

By default the binary is given the generic moniker "gocmd". Using --cat, we can see that the code was wrapped in a main function and the fmt package was imported for us. Using the --code option, Goscript will automatically add imports for packages in the standard library and any packages listed in the project's imports.json file. The imports.json file also enables specifying a different package alias (e.g. "re" rather than "regexp").

> $ goscript --cat gocmd                                                 
#!/usr/bin/env -S goscript
package main

import (
    "fmt"
)

func main() {
    fmt.Println("Hello Goscript!")
} 

Note also that --cat added a shebang (#!/usr/bin/env -S goscript) to the top of the file in the event your intent was to execute locally as a script. More on that later.

Since the [project]/bin folder is on the PATH, you can execute the compiled command directly.

> $ gocmd                                                              
Hello Goscript!

More often than not, you'll want to give your command a unique name with the --name option. The --cat option can also be used to copy an existing command to a new name.

> $ goscript --cat gocmd --name greet                                         
A copy of gocmd was saved as greet

If you use an IDE, such as VSCode, you are probably used to a high level of support when writing Go code. If you try to edit locally (ie. outside of your project), your IDE may not have the proper context to provide you that support. The --edit option enables you to easily open the command source within the project directory in your favorite editor.

> $ goscript --edit greet                                                    

VSCode_greet

We can then edit the file to take a command-line argument and recompile.

package main

import (
    "fmt"
    "os"
)

func main() {
    fmt.Printf("Hello %s!\n", os.Args[1])
}

You can recompile a specific command simply by passing the --name option, or all commands using --recompile.

> $ goscript --recompile                                                     

> $ greet World                                                               
Hello World!

If you prefer, you can --export the greet command as a script, complete with a working shebang line (similar to the --cat example shown above). This will also do a "soft delete" in the project, removing the binary but saving a copy of the source without the .go extension, making it effectively invisible to the Go compiler. That renamed source file will be shown if you --list commands, pointing out the option to --restore the command.

> $ goscript --export greet > greet.gs                                                                                                                     
> $ goscript --list                                                         
gocmd
greet (requires --restore)

We can modify the permissions on the "greet.gs" file we just created and execute it as a script.

> $ chmod 744 greet.gs                                                                                                                                  
> $ ./greet.gs Shebang                                                         
Hello Shebang!

If you need to use a package outside Go's standard library, there is a --goget option to pull it into the project. However, since Go's package convention is geared toward online repos (e.g. github), Goscript will often be able to automatically go get external packages that are imported, adding them to both the go.mod file and the imports.json file. Goscript will also run go mod tidy to clean up go.mod when using the --export or --delete options.

As an example, let's write a simple uppercase utility that imports the github.com/bitfield/script package. We can write this outside the project in a file called "tmp.go" (Note, the .go extension isn't required).

package main

import (
    "github.com/bitfield/script"
    "strings"
)

func main() {
    script.Stdin().FilterLine(strings.ToUpper).Stdout()
}

We can use --file and --name to import the code to the project with the name "upper".


> $ goscript --file tmp.go --name upper

> $ echo uppercase | upper                                                    
UPPERCASE

Executing the code demonstrates that the external package was added, but we can also view the updated go.mod file using the --dir option to reference the project location.

> $ cat `goscript --dir`/go.mod                                               
module goscripts

go 1.22.1

require (
    github.com/bitfield/script v0.24.1 // indirect
    github.com/itchyny/gojq v0.12.13 // indirect
    github.com/itchyny/timefmt-go v0.1.5 // indirect
    mvdan.cc/sh/v3 v3.7.0 // indirect
)

Likewise, the imports.json file now includes a mapping for "github.com/bitfield/script" to support automatic import with the --code option when using the alias "script".

> $ cat `goscript --dir`/imports.json                                        
{
    "script": "github.com/bitfield/script"
}

And, as you can see from that last example, bitfield/script makes it easy to write commands that can be combined in a pipeline just like built-in Unix utilities.

> $ ./greet.gs Pipes | upper                                                  
HELLO PIPES!

Conclusion

Scripting in Go is fun and powerful. Goscript can make it more convenient to use Go for scripts. To get started, visit the repo at https://github.com/fkmiec/goscript.