Have you ever wanted to use Go to write shell scripts?
Where you could put a "shebang" line like #!/usr/bin/env go
, mark your source file executable, and run it?
I've found a pretty elegant way of making this possible, all in pure Go!
To write your own Go scripts, just include this shebang header (vim modeline optional). Your shell script does not need a *.go
extension. In fact you're better off not having a *.go
extension!
///bin/true && exec /usr/bin/env script.go "$0" "$@"
// vim:set ft=go:
And then put this file,
script.go
, in your $PATH
and make it executable:
///bin/true && exec /usr/bin/env go run "$0" "$@"
//
// script.go is a Go program that can run itself as a shell script, as well as
// help other Go scripts to run themselves.
//
// To make your Go scripts work with this, use the following "shebang" header
// at the top of your script:
//
// ///bin/true && exec /usr/bin/env script.go "$0" "$@"
// // vim:set ft=go:
// package main
//
// The first line will cause your shell to run `script.go` passing the current
// filename and the rest of the command line arguments. The vim modeline comment
// may help your code editor to highlight the file as Go syntax.
package main
import (
"crypto/rand"
"encoding/hex"
"flag"
"fmt"
"io/ioutil"
"os"
"os/exec"
"os/signal"
"path/filepath"
"syscall"
)
// Version of this script.
const Version = "1.0.0"
// Command line arguments
var (
debug bool
version bool
)
func init() {
flag.BoolVar(&debug, "debug", false, "Verbose debug logging")
flag.BoolVar(&version, "version", false, "Show version number and exit")
}
func main() {
flag.Parse()
if version {
fmt.Printf("This is script.go v%s\n", Version)
os.Exit(0)
}
// Parse the script name and remaining arguments.
args := flag.Args()
if len(args) == 0 {
usage()
}
scriptName := args[0]
argv := args[1:]
// Verify it's a file.
if _, err := os.Stat(scriptName); os.IsNotExist(err) {
die("%s: not a file", scriptName)
}
// Make a temp file with a *.go extension
tmpfile, err := NamedTempFile("", "script", ".go")
if err != nil {
die("tempfile error: %s", err)
}
log("scriptName: %s; tmpFile: %s", scriptName, tmpfile.Name())
// Read the source and write it to the new file.
src, err := ioutil.ReadFile(scriptName)
dieIfError(err)
_, err = tmpfile.Write(src)
dieIfError(err)
err = tmpfile.Close()
dieIfError(err)
// Catch interrupt signals to clean up the tempfile.
interrupt := make(chan os.Signal, 2)
signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM)
go func() {
<-interrupt
log("interrupt detected; cleaning up tempfile")
err = os.Remove(tmpfile.Name())
if err != nil {
die("remove tmpfile error: %s", err)
}
}()
// Finally, `go run` the script from $TMPDIR.
goArgs := append([]string{"run", tmpfile.Name()}, argv...)
c := exec.Command(
"go",
goArgs...,
)
c.Stdin = os.Stdin
c.Stdout = os.Stdout
c.Stderr = os.Stderr
err = c.Run()
if err != nil {
fmt.Printf("[script.go] script error: %s\n", err)
}
log("cleaning up tempfile")
os.Remove(tmpfile.Name())
}
// handler for Ctrl-C cleaning up the temp file.
func cleanup() {
fmt.Println("cleanup")
}
// NamedTempFile is like ioutil.TempFile but accepts a suffix too.
func NamedTempFile(dir, prefix, suffix string) (f *os.File, err error) {
if dir == "" {
dir = os.TempDir()
}
// Random string generator.
randomString := func() string {
randBytes := make([]byte, 16)
rand.Read(randBytes)
return hex.EncodeToString(randBytes)
}
for i := 0; i < 10000; i++ {
name := filepath.Join(dir, prefix+randomString()+suffix)
f, err = os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
if os.IsExist(err) {
continue
}
break
}
return
}
func usage() {
fmt.Print(
"Usage: script.go [options] <go script path>\n",
"See script.go -h for command line options.\n",
)
os.Exit(0)
}
func log(message string, v ...interface{}) {
if debug {
fmt.Printf("[script.go] "+message+"\n", v...)
}
}
func die(message string, v ...interface{}) {
fmt.Printf(message+"\n", v...)
os.Exit(1)
}
func dieIfError(err error) {
if err != nil {
die(err.Error())
}
}
Links to an example on my .dotfiles
repo:
*.go
file in ~/bin
.Let me tell you about the adventure getting here.
My first pass at this was to use a Python script to run my Go scripts. I wrote a Python script named gosh
(for "Go shell") and put it in my $PATH
(at $HOME/bin
).
Now I could start a Go script with #!/usr/bin/env gosh
and running the Go script would end up passing it into gosh
, where I could then find a way to make the script go run
nable.
The Python script would read in the source file, chop off the shebang line (because #!
would be invalid syntax in Go), write it into a new empty directory in /tmp
with a *.go
extension (because go run
won't run it otherwise), and finally go run /tmp/whatever/script.go
while passing the remaining command line arguments to it.
It worked well enough, but having the #!
shebang line in my Go script broke my text editor. As it wasn't valid Go code, a lot of my Go plugins weren't able to work with it, so I'd have to temporarily remove the shebang line while working on it, and probably add a *.go
extension temporarily for easy and quick write-run cycles.
These weren't ideal, and having a dependency on Python to run a Go script wasn't ideal either.
Recently, I Googled to see what other people are doing to solve this problem, and found the closest thing to a shebang line that Go will natively accept.
For very simple use cases, name your script with a *.go
suffix and begin it like this:
///bin/true; exec /usr/bin/env go run "$0" "$@"
package main
As it turns out, when you ./main.go
your script, your shell will initially guess it's a Bash script and try running it as such. So that first line tells Bash to run /bin/true
(a no-op that returns a success code) and then use exec
to replace its process with go run
and passes its file name to it ($0
) and the remaining command line arguments ($@
).
Go then runs the script, ignoring the "shebang line" because it's a valid comment in Go, and happily builds and runs the program.
There are a couple of problems with this:
*.go
suffix; otherwise go run
ignores them.*.go
with package main
and func main()
and start bothering you with complaints about duplicate names and all sorts of problems.But this basic shebang line to run *.go
scripts with go run
gave me a better idea.
I could combine the best of the above solutions. Replace the Python gosh
script with a Go script that self-runs itself with go run
, and then have my Go shell scripts run that Go script to run themselves.
I called the Go shell script runner script.go
and its source is at the top of this post.
This solves the problems of the above go run
method:
*.go
extension, and to make it easier on you, they should not have one.*.go
script in my bin folder is script.go
, so my text editor doesn't freak out. It only sees one func main()
and such because there is only one *.go
file.The last problem may be the syntax highlighting; without a *.go
extension your editor might not detect it as Go code.
For this, I put a Vim modeline in, so opening it in Vim will automatically set the Go syntax highlighting. For Atom, I installed vim-modeline.
Atom still works fine with my Go scripts, running go fmt
and goimports
and criticizing my syntax, but it doesn't confuse the sources of all of my Go scripts and getting in my way.
You can check out the sources here:
There are 0 comments on this page. Add yours.
0.0121s
.