Newsletter
TechAnV Blog
Get updates on security engineering, Rust, eBPF, and DevSecOps. No spam, unsubscribe anytime.
Check your inbox and click the confirmation link to complete your subscription.
A shell script for running Go one-liners#
bitfield/script is a really neat Go project: it tries to emulate shell scripting using Go chaining primitives, so you can run code like this:
1script.Stdin().Column(1).Freq().First(10).Stdout()To achieve the same thing as:
1cat file.txt | cut -f1 | sort | uniq -c | sort -nr | head -10A comment from jvictor118 on Hacker News:
If one were actually going to use something like this, I’d think it’d be worth implementing a little shebang script that can wrap a single-file script in the necessary boilerplate and call go run!
This is exactly the kind of thing I can’t quite be bothered to write myself, but I’m happy to coach GPT-4 through building.
The result: goscript.sh. You can use it like this:
1cat file.txt | ./goscript.sh -c 'script.Stdin().Column(1).Freq().First(10).Stdout()'Or you can create a script file like this one, saved as top10.goscript:
1script.Stdin().Column(1).Freq().First(10).Stdout()And run:
1cat file.txt | ./goscript.sh top10.goscriptFinally, you can set the shebang line in a script file like this:
1#!/tmp/goscript.sh2script.Stdin().Column(1).Freq().First(10).Stdout()Then run this:
1chmod 755 top10.goscript2cat file.txt | ./top10.goscriptThe script#
Here’s the goscript.sh script that GPT-4 and I came up with:
1#!/bin/bash2
3TMPDIR=$(mktemp -d /tmp/goscript.XXXXXX)4SUBDIR="$TMPDIR/goscript_inner"5mkdir -p $SUBDIR6trap "rm -rf $TMPDIR" EXIT7
8TMPFILE="$SUBDIR/script.go"9
10# Write boilerplate to tmpfile11cat > $TMPFILE <<EOF12package main13
14import (15 "github.com/bitfield/script"16)17
18func main() {19EOF20
21# Check for -c flag22if [ "$1" == "-c" ]; then23 # Add the literal string from argument24 echo "$2" >> $TMPFILE25else26 # Add user's code from file27 sed '/^#!/d' "$1" >> $TMPFILE28fi29
30# Close main function31echo "}" >> $TMPFILE32
33# Initialize a new module in subdir, fetch dependencies, and run34pushd $SUBDIR > /dev/null 2>&135go mod init tmp > /dev/null 2>&136go get github.com/bitfield/script > /dev/null 2>&137go run script.go38popd > /dev/null 2>&1And the full ChatGPT transcript that lead to the final script presented here.
(Missing from that transcript is the final step where we added the sed line to strip out the shebang.)
Here’s what I learned from the above code.
The program itself is wrapped in the following boilerplate, using >> to write to the temporary file:
1package main2
3import (4 "github.com/bitfield/script"5)6
7func main() {8 // User's code goes here9}With modern Go you need to use the following pattern to get something like this to work with a dependency:
1go mod init tmp2go get github.com/bitfield/script3go run script.gogo get downloads the dependency, using a cache if it’s already been downloaded.
The shell script runs all of that in a temporary directory, created using:
1TMPDIR=$(mktemp -d /tmp/goscript.XXXXXX)2SUBDIR="$TMPDIR/goscript_inner"3mkdir -p $SUBDIRThat mktemp -d /tmp/goscript.XXXXXX line uses the templating feature of mktemp, where a sequence of XXX is replaced by random characters.
The trap call is interesting - see also Running multiple servers in a single Bash script. Effectively it ensures the temporary directory is deleted when the script terminates, no matter why it terminates (success or error):
1trap "rm -rf $TMPDIR" EXITI wanted to support two ways of calling the script:
1./goscript.sh -c 'go code here'2./goscript.sh script.goscriptThat’s handled by this conditional check:
1if [ "$1" == "-c" ]; then2 # Add the literal string from argument3 echo "$2" >> $TMPFILE4else5 # Add user's code from file6 sed '/^#!/d' "$1" >> $TMPFILE7fiThe sed line is necessary because if you have a script that looks like this:
1#!/tmp/goscript.sh2script.Stdin().Column(1).Freq().First(10).Stdout()That first line will be copied into the Go code in a way that breaks syntax. Using sed here strips that line out before copying the rest of the file into the main() function in the boilerplate.
With this accounted for, running ./top10.goscript effectively runs the same as calling ./goscript.sh top10.goscript.
Several of the commands in the script output information to stdout or stderr - we fixed that with this pattern:
1go mod init tmp > /dev/null 2>&1(It feels weird to refer to the combination of myself and GPT-4 as “we”, but I think it’s an honest description of the we we collaborated to build this.)