Make
-ing things vs. general purpose tasks
Make is a tool use to build and transform source code. It excels at keeping track of input files and output files from build steps, and over time it’s been used (by me!) as more of a general purpose task tool. A seminar read for me, coming from the geospatial side, was Mike Bostock’s 2013 article wherein he showed how to benefit from the dependency graph of ’tasks’ to get stuff done. That was a game changer.
But over the years it has also felt like a hassle in terms of the special syntax for handling variables and the Bash-like-but-exactly-bash syntax for strings.
And ultimately I found myself just wanting a way to run ’tasks’ rather than file and code transformations specifically.
Something more general purpose.
I’ve toyed with using more dynamic scripting-style languages for automation, but to be honest the idea of installing all of Ruby for a Rake file or Python for something like a setup.py
seemed like a massive overhead.
Especially if the project does not call for either of those languages.
Enter Task
for Tasks
On more recent projects I’ve been trying to use and learn Go.
No particular reason other than I like the idea of a single distributable binary, and I’m a crap programmer so statically typed languages that fail at compile time are better for me (and you!).
And while searching the web for a decent Go-based ’task runner’-type utility I came across Task.
task
is a utility written in Go, and I can’t do much better than their own intro to explain why it promised to scratch my itch so perfectly:
Task is a task runner / build tool that aims to be simpler and easier to use than, for example, GNU Make.
Since it’s written in Go, Task is just a single binary and has no other dependencies, which means you don’t need to mess with any complicated install setups just to use a build tool.
Well that fits the bill!
The basic premise is that you write a YAML file called a Taskfile.yml
in your project directory, then run the task [taskname]
command to get it done.
The major painpoint it removes for me is a much more modern and consistent way to handle strings, data, variables, and dependencies between tasks when compared to Make.
Installation
I’m running a Mac right now, so the canonical way to install is using Homebrew:
|
|
But since I’m already running Go (using asdf
) I found the Go-based approach simpler:
|
|
NOTE: I did also install the shell completions which just make everything that much nicer.
My setup
I’m working on a learning project called invest
right now.
It’s a little website that depends on some YAML file downloading, processing, sticking into SQLite, and then consuming via a Go HTTP server with a little frontend.
There are several steps that I repeat frequently while working on this project.
A good time to write my first Taskfile.yaml
!
Here’s what I’ve got:
Taskfile.yaml
|
|
So I can run task --list
or just task
to see what’s available to me:
|
|
Taskfile Details
There’s a fair amount going on in that but ultimately it’s pretty simple and even has a lot of the same top-level ’tasks’ that I typically use in a Makefile. Here are the things worth paying attention to.
vars
are simple key/value pairs and don’t have a bunch of single- or double-quotes to worry about, courtesy of YAML.vars
are referred to by their name with a preceding.
soTAGS_CSV
becomes.TAGS_CSV
.- String interpolation relies on the conventions in Go’s
text/template
standard library. So if you know how to write Go templates you’re done, and they not hard to learn. If you look at the monstrosity of ayq
call on line 39, for example, you see that I just need to use{{ .INVEST_DATA }}
to make things work. No weird double-quotes (or is it single-quotes?) that Make would require. tasks
have descriptions, commands, dependencies, and statuses.tasks
can beinternal
meaning they don’t show up on the list of possible tasks at the command line, and you also cannot run them explicity. This keeps the file cleaner. Case in point, checkout line 72 where thetmp-dir
task isinternal: true
.cmds
are nothing more than commands you would run in your shell. So if something runs how you want it to in your shell, there’s a really high probability that it’ll Just Work in acmd
. That’s a radically better experience over my typical Make tribulations. Yes, you have to understand the Go templating, but again that seems really predictable to me.- Status. Like Make, you often don’t want to run a task unless you need to. That’s what the
status
instruction does on line 26-27. I do not want to download the employee data again if I already have it. So thetest -f the-data-file
returns true if I have the file, and thefetch-data
command doesn’t run any of thecmds
, saving me some time and bandwidth. deps
(dependencies) can be declared and run in the order that they are listed. That’s worth repeating because, naturally, I forgot that I read that after reading the docs and was scratching my head for a while.cmds
are composable. Look at line 48. That’s where I am calling theparse
task from within bydb
task. Again, order is important. To be honestly, I should probably makeparse
aninternal: true
task, but it was such a bugger to get thatyq
stuff working that I wanted to have it handy just by typingtask parse
.CLI_ARGS
is your typical--
magic way to inject extra stuff into a command. For example at line 62 you can see me leaving the option for adding anygo test
options. I’d do that by sayingtask test -- -v
, which would pass the-v
(verbose) argument seamlessly into my test step.0
More to discover
Task is also incredibly well documented, and I see plenty of options for future expansion.
For example, the Go-inspired defer
keyword is a lovely addition.
And the general approach to avoiding expensive work is well thought-out.
I encourage you to take a look.
Task is not only cross platform, single-binary, easy installation.
It is also a focused, simple, and pleasant experience…especially when compared with Make.