Introduction to the Taskfile format

Adrian Cooney
6 min readNov 11, 2016

--

We need to talk about our task runners. It seems everyone, like myself, has jumped ship from Gulp (after jumping ship from Grunt not too long ago) to npm run-scripts. If you’re like me then you probably appreciate the simplicity of npm’s run-script functionality. It’s a no-fuss, no-bloat way to run tasks right from your favourite package manager (or is it? Hello yarn!). It has one serious problem though, no one should be writing shell scripts inside a JSON file. No one.

It’s only recently, however, that I’ve become frustrated with npm’s run-script. It’s served me well for small projects and packages but for large projects with multiple interdependent tasks, it’s absolutely painful. Writing the build or testing scripts inside the package.json is claustrophobic, error-prone and just plain frustrating. So much so that I often find myself longing for the whitespace and logical separation of tasks like Gulp with it’s Gulpfiles. It’s only after I install gulp and the 15+ unmaintained and outdated plugin dependencies to build my project that I forgive npm and force myself back into the package.json hole.

I decided to come up with something different. I wanted something that was as simple as npm run with the power of Gulp’s task definitions using the tools we already have. Was that so much to ask? It was at this crossroads that I managed to find a format that I feel hits that sweet spot between simplicity and power. Found being the keyword. It’s using tools and methods that have been around forever but in a way that works so well for myself, I feel more people should use it. Consider this a guide to enlightenment rather than creating anything new. You could call it a format.

The Taskfile

Each new projects of mine starts with a simple bash script called a Taskfile. It sits in the root of your projects directory beside your package.json or gulpfile.js. The Taskfile has three basic parts:

  • At the top of our Taskfile, we have the bash shebang to describe how we should interpret the rest of the file (your can use your own favourite shell like sh, csh, zsh or fish if you prefer).
  • The body of our Taskfile is a list of functions that are our tasks. The name of the function is the name of the task.
  • At the end of the file, we have “$@” line that, if you know your bash, is a variable that contains the arguments passed to the script (minus the name of the program, $0). By calling “$@”, we’re telling bash to execute the arguments as if it were a command itself within the script.

See an example Taskfile task below:

To start the task, we call the Taskfile with the name of our task we want to execute.

$ ./Taskfile serve
Serving HTTP on 0.0.0.0 port 8000 ...

The serve task will serve the contents of our project via a simple HTTP server. That, in essence, is our simple task runner.

Arguments

Let’s pass some arguments to a task. Arguments are accessible to the task via the $1, $2, $n .. variables. Let’s allow us to specify the port of the HTTP server:

$ ./Taskfile serve 9090
Serving HTTP on 0.0.0.0 port 9090 ...

Using npm Packages

One of the most powerful things about npm run-scripts (who am I kidding, it’s definitely the most powerful thing) is the ability to use the CLI interfaces for many of the popular packages on npm such as babel or webpack. The way npm achieves this is by extending the search PATH for binaries to include ./node_modules/.bin. We can do this to very easily too by extending the PATH at the top of our Taskfile to include this directory. This allows us to use our favourite binaries just like we would in any npm run-script:

Task Dependencies

Sometimes tasks depend on other tasks to be completed before they can start. Call task dependencies is as simple as calling their function. To add another task as a dependency, simply call the task’s function at the top of the dependant task’s function.

Please don’t every deploy like that. Ever.

Parallelisation

To run tasks in parallel, you can us Bash’s & operator in conjunction with wait. The following will build the two tasks at the same time and wait until they’re completed before exiting.

And execute the build-all task:

$ run build-all
beep web boop
beep mobile boop
built web
built mobile

Default task

To make a task the default task called when no arguments are passed, we can use bash’s default variable substitution ${VARNAME:-<default value>} to return default if $@ is empty.

Now when we run ./Taskfile, the default function is called.

Runtime Statistics

To add some nice runtime statistics like Gulp so you can keep an eye on build times, we use the built in time and pass if a formatter.

And if we execute the build task:

$ ./Taskfile build 
beep boop built
Task completed in 0m1.008s

Help

The final addition I recommend adding to your base Taskfile is the help task which emulates, in a much more basic fashion, npm run (with no arguments). It prints out usage and the available tasks in the Taskfile to show us what tasks we have available to ourself.

The compgen -A function is a bash builtin that will list the functions in our Taskfile (i.e. tasks). This is what it looks like when we run the help task:

$ ./Taskfile help
./Taskfile <task> <args>
Tasks:
1 build
2 default
3 help
Task completed in 0m0.005s

Cool Runnings

So typing out ./Taskfile every time you want to run a task is a little lousy. npm run just flows through the keyboard so naturally that I wanted something better. The solution for less keystrokes was dead simple: add an alias for run (or task, whatever you fancy) and stick it in your .zshrc. Now, it now looks the part.

$ alias run=./Taskfile
$ run build
beep boop built
Task completed in 0m1.008s

Quickstart

Alongside my run alias, I also added arun-init to my .zshrc to quickly get started with a new Taskfile in a project. It downloads a small Taskfile template to the current directory and makes it executable:

$ alias run-init="curl -so Taskfile https://raw.githubusercontent.com/adriancooney/Taskfile/master/Taskfile.template && chmod +x Taskfile"$ run-init
$ run build
beep boop built
Task completed in 0m1.008s

Free Features

  • Conditions and loops. Bash and friends have support for conditions and loops so you can error if parameters aren’t passed or if your build fails.
  • Streaming and piping. Don’t forget, we’re in a shell and you can use all your favourite redirections and piping techniques.
  • All your standard tools like rm and mkdir.
  • Globbing. Shells like zsh can expand globs like **/*.js for you automatically to pass to your tools.
  • Environment variables like NODE_ENV are easily accessible in your Taskfiles.

Considerations

When writing my Taskfile, these are some considerations I found useful:

  • You should try to use tools that you know users will have installed and working on their system. I’m not saying you have to be POSIX.1 compliant but be weary of using tools that aren’t standard (or difficult to install).
  • Keep it pretty. The reason for the Taskfile format is to keep your tasks organised and readable.
  • Don’t completely ditch the package.json. You should proxy the scripts to the Taskfile by calling the Taskfile directory in your package.json like "test": "./Taskfile test". You can still pass arguments to your scripts with the -- special argument and npm run build -- --production if necessary.

Caveats

The only caveat with the Taskfile format is we forgo compatibility with Windows which sucks. Of course, users can install Cygwin but one of most attractive things about the Taskfile format is not having to install external software to run the tasks. Hopefully, Microsoft’s native bash shell in Windows 10 can do work well for us in the future.

FAQ

How is this different to Make and Makefiles?

I’ve made valiant efforts to use make as a replacement for npm run-scripts and Gulp but I just don’t feel it works well as a task runner. Make keeps track of changed files and only runs targets if files have changes. This doesn’t really suit tasks that don’t require any files such as starting a dev server or deploying. Among quirks like the tab indentation as a hard requirement, not being able to pass arguments and the .PHONY confusion, I just didn’t feel it compared.

Collaboration

The Taskfile format is something I’d love to see become more widespread and it’d be awesome if we could all come together on a standard of sorts. Things like simple syntax highlighting extensions or best practices guide would be awesome to formalise. There’s a Github repo that contains a basic template and the techniques mentioned above so feel free to collaborate there with issues and comments:

http://github.com/adriancooney/Taskfile

Thanks for reading!

--

--