Skip to content

zcag/goscript

Repository files navigation

goscript

Run small, standalone Go scripts without creating a module or repository. Supports importing any Go module automatically — no go.mod required.


Inline mode

Run Go snippets directly — just pass the code as an argument:

goscript 'fmt.Println("hello")'

Auto-print

The last expression in your snippet is automatically printed — no fmt.Println needed:

goscript '"hello " + "world"'     # prints: hello world
goscript 'os.Getenv("HOME")'      # prints: /home/you
goscript 'time.Now().Year()'      # prints: 2025

Explicit fmt.Print* calls are detected and left untouched (no double-printing).


Script mode

Write a single executable file with a shebang and run it like a script.

#!/usr/bin/env goscript
package main

import (
    "fmt"
    "os"
    "github.com/pkg/errors" // external deps installed automatically
)

func main() {
    fmt.Println("args:", os.Args[1:])
    fmt.Println(errors.New("boom"))
}
chmod +x myscript
./myscript a b c

# Compile to a standalone binary
goscript -o mybin myscript
./mybin a b c

# Graduate script to a proper Go module
goscript -m myproject/ myscript

Scripts must use package main and define func main() — it's real Go, not a DSL.


Pipe mode (automatic)

When your snippet references any of these magic variables, pipe mode is activated automatically — no flags required:

Variable Type Value
x / line string current input line
i / idx int line index (0-based)
f / fields []string whitespace-split fields of x
lines []string all lines (read before the loop)

Per-line processing

Reference x or line → loops over stdin line by line:

ls | goscript 'strings.ToUpper(x)'
ps aux | goscript 'f[0]'                    # first field of each line
cat data.csv | goscript 'f[1]' -F ','       # CSV second column
cat file.txt | goscript 'fmt.Sprintf("%3d  %s", i, x)'  # add line numbers

Batch processing

Reference lines → reads all stdin first, then runs your code once:

wc -l < file.txt                               # shell way
cat file.txt | goscript 'len(lines)'           # Go way, auto-printed

# Sort lines
cat file.txt | goscript 'sort.Strings(lines); strings.Join(lines, "\n")'

# Count unique words
cat file.txt | goscript '
    counts := map[string]int{}
    for _, l := range lines { counts[l]++ }
    pp(counts)
'

Field separator (-F)

-F sep changes how f/fields is split. Default is whitespace (like strings.Fields).

cat data.csv | goscript 'f[2]' -F ','       # third CSV column
cat /etc/passwd | goscript 'f[0]' -F ':'    # usernames

Parallel processing (-j N)

-j N runs the loop body across N goroutines (output order not guaranteed):

cat urls.txt | goscript 'fetch(x)' -j 20

# Square numbers in parallel
seq 100 | goscript 'atoi(x) * atoi(x)' -j 8

Regex pipe mode (-r pattern)

-r pattern compiles a regex and activates pipe mode. Only lines matching the pattern are processed (non-matching lines are skipped). Inside the loop:

Variable/func Type Description
r *regexp.Regexp the compiled regex
m []string submatches: m[0]=full match, m[1+]=capture groups
n map[string]string named capture groups
sub(repl, s) string replace first match (supports $1, $2, ${name})
suball(repl, s) string replace all matches
# filter to matching lines only
cat file | goscript -r '\d+' 'x'

# extract first capture group
cat file | goscript -r '(\d+)' 'm[1]'

# named capture groups
cat /etc/passwd | goscript -r '(?P<user>[^:]+):.*:(?P<shell>[^:]+)$' \
  'n["user"] + " → " + n["shell"]'

# replace first match
cat file | goscript -r '(\w+)@(\w+)' 'sub("$1 at $2", x)'

# replace all matches
cat file | goscript -r '\d+' 'suball("NUM", x)'

# use r directly for full regexp API
cat file | goscript -r '\w+' 'r.ReplaceAllStringFunc(x, strings.ToUpper)'

Helpers

Every inline/pipe/batch script gets these functions for free:

Function Description
die(err) Print error and exit 1. No-op if err is nil.
must(v, err) Generic unwrap: n := must(strconv.Atoi(s))
atoi(s) Parse int, exit on failure.
atof(s) Parse float64, exit on failure.
pp(v) string Format any value as indented JSON. Auto-print friendly.
trim(s) strings.TrimSpace(s) shortcut.
splitlines(s) Split a multi-line string into []string.
match(pat, s) []string of submatches, nil if no match. Cached.
named(pat, s) map[string]string of named captures, nil if no match. Cached.

Examples:

seq 5 | goscript 'atoi(x) * atoi(x)'        # squares
echo '{"a":1}' | goscript 'pp(map[string]int{"x": 42})'

# match/named without -r (handle nil yourself for non-matching lines)
cat file | goscript 'match(`v(\d+)`, x)[1]'
cat file | goscript 'named(`(?P<maj>\d+)\.(?P<min>\d+)`, x)["maj"]'

Inspect generated code (--explain)

Print the Go source that would be compiled, without actually running it:

ls | goscript --explain 'strings.ToUpper(x)'
goscript --explain 'pp(os.Environ())'

Useful for understanding what's generated or debugging unexpected output.


Cache control

goscript --no-cache 'fmt.Println("fresh compile")'   # skip cache, always recompile
goscript --clear-cache                                # wipe entire cache

Cache lives at ~/.cache/goscript/ (respects $XDG_CACHE_HOME):

~/.cache/goscript/
├── bin/<hash>/app      ← compiled binary
└── work/<hash>/
    ├── main.go
    └── go.mod

The cache key is the SHA-256 of the generated source, so any change to your snippet produces a fresh compile.


Installation

go install github.com/zcag/goscript@latest

Make sure $GOPATH/bin (or $HOME/go/bin) is in your PATH.


How it works

  1. Source is read (from file or inline snippet)
  2. For inline mode: magic variables are detected → appropriate template is chosen
  3. Auto-print: if the last statement is a bare expression, it's wrapped in fmt.Println
  4. Source is hashed → cache is checked
  5. On miss: imports are resolved (via goimports), go.mod is generated, binary is compiled
  6. Compiled binary is executed (or printed with --explain, or saved with -o)

Subsequent runs with the same source reuse the cached binary instantly.

About

Go scripting, write single file executable, run compiled. Shebang magic.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors