Scripting Utilities for Go projects

A complementary bunch of tiny modules.

March 25, 2024

I've been programming in Go since 2013-ish, but I took a sort of sabbatical as a craftsman at Clean Coders Studio doing a lot with Clojure, which introduced me to the beautiful world of functional programming. Now, back in Gopherland I have found myself craving more functional-style building blocks. Also, the nature of my work since then has required me to write literally hundreds of scripts in Go, as opposed to long-lived services and enterprise software. When writing scripts the rules are different--you don't have to gracefully handle errors (you just kill the program). So, I've built several loosely related utilities to facilitate scripting in Go in a functional style, all of which I present to you here:

github.com/mdwhatcott/go-cli-template

I enjoy building small, simple CLI tools. This template is a pretty decent starting point for me, and it's compatible with the gonew utility. This template includes almost all of the libraries listed below.

github.com/mdwhatcott/tui

Sometimes using a CLI is tedious ('what are the flags and options?') and I'd instead prefer that the program guide me through a series of UI elements and steps. This little repo is a no-frills text UI toolkit featering prompts, select menus, and confirmation dialogs. By default it interacts with os.Stdin and os.Stdout, but is compatible with any io.Reader/Writer combination.

github.com/mdwhatcott/slogging

A simple library to extend the output of the new "log/slog" package released in Go 1.21. Options exist for:

github.com/mdwhatcott/exec

Admittedly, a hack that employed bash -c ... to execute arbitrary shell commands from Go without needing to quote each argument. Maybe be careful with this one.

import "github.com/mdwhatcott/exec"

func main() {
	output, err := exec.Run("ls -1",
		exec.Options.At(folderPath),
		exec.Options.Out(writer),
	)
	// ...
}

github.com/mdwhatcott/funcy

Many clojure.core functions implemented in Go with generics to faciliate functional-style programming, with apologies to Go purists who would rather just use for loops, as well as functional programmers who expect lazy behavior similar to what is offered in Clojure and other functional languages. Here's a short example of how I would use the funcy package to solve Project Euler Problem #6:

func main() {
	fmt.Println(Calculate006(10))  // 2640
	fmt.Println(Calculate006(100)) // 25164150
}
func Calculate006(n int) int { return SquareOfSums(n) - SumOfSquares(n) }
func SquareOfSums(n int) int { return Square(Sum(Range(1, n+1))) }
func SumOfSquares(n int) int { return Sum(Map(Square[int], Range(1, n+1))) }

github.com/mdwhatcott/must

For anyone who is tired of writing scripts and having to deal with error handling of the form:

thing, err := someFunction()
if err != nil {
	panic(err)
}

When working backend enterprise software (web servers, daemons, etc.) which are long-lived, error handling cannot be avoided, but when you're writing a script, crashing fast is often very desirable, so error handling becomes very repetitive (see above snippet). With must, you can write this:

thing := must.Value(someFunction())

... and trust that if an error surfaces, the program will panic and (probably) crash.

This module also defines sub-packages that correspond with standard library packages:

file := osmust.Create(path)
content := jsonmust.Marshal(stuff)
// etc..

github.com/mdwhatcott/go-set

Try a few queries on pkg.go.dev and you'll quickly find more implementations of set data structures than you'll ever need. It's clear we programmers like working with sets. A proposal to add sets to the standard library was started, but has since been locked. Who knows why (I tried skimming the discussion thread, but it got way too long and convoluted). So, not to be outdone, I decided to muddy the waters by creating my own implementation.

I might merge this with the funcy module someday because I so often use these modules together.

github.com/mdwhatcott/tiny-should

Sometimes I don't need my trusty x-unit test runner. I just need a few simple assertion helpers. That's what this package is for:

func TestPassing(t *testing.T) {
	should.So(t, 1, should.Equal, 1)
	should.So(t, false, should.BeFalse)
	should.So(t, true, should.BeTrue)
	should.So(t, nil, should.BeNil)
	should.So(t, 1, should.NOT.BeNil)
	now1 := time.Now()
	now2 := now1.In(time.UTC)
	should.So(t, now1, should.Equal, now2)
}
func TestFailing(t *testing.T) {
	t.Skip("comment me to see the failures below")
	should.So(nil, 1, should.Equal, 2)
	should.So(t, 1, should.Equal, 2)
	should.So(t, true, should.BeFalse)
	should.So(t, false, should.BeTrue)
	should.So(t, 1, should.BeNil)
	should.So(t, nil, should.NOT.BeNil)
	should.So(t, uint64(1), should.Equal, uint64(2))
	should.So(t, time.Now(), should.Equal, time.Now())
}

-Michael Whatcott