There are a few common things I end up doing in all my projects, independently of the programming language I use. One of them is a text file with notes, TODOs, and design ideas. The other is a task file. This is about the latter.

A task file is shell script with a collection of commands that I use to build, test, sync and do whatever else is needed for the project. Think of it as a mix of an annotated bash history of the project and a Makefile.


Every task file is usually in the root of the project, and this is the basic skeleton of one:

set -euo pipefail

# add functions here...

function help {
  echo "$0 <task> <args>"
  echo "Tasks:"
  compgen -A function | grep -v "^_" | cat -n


Let’s go through the relevant lines.

set -euo pipefail

sets 3 options for the shell:


sets the internal field separator to newline and tab. This is usually what I want when doing any for loops over files, etc…

The help function lists all functions available on the file as commands, except if they start if “_”, which I use for rare internal functions.

The final line tries to call whatever function is passed as the first parameter, and defaults to help.

I used to also time the task, by replacing the last line with:

TIMEFORMAT="Task completed in %3lR"
time "${@:-help}"

But I since moved a version of this to my prompt.

How to use

The way I use task is to simply populate with high level functions I care about in a project. It’s a great way to document things for my future self.

For example, on this site, I have commands like:

function watch {
  lume --watch "$@"

function build {
  lume "$@"

Most of the times they are one or two liners, but sometimes they grow into 10-20 lines scripts.

And then call it directly from the root of the project:

$ ./task build


Another advantage of always using task as the build, is that I can have zsh autocomplete based on the help function above.

This is the relevant part of my .zsh config:

function _task {
  ARGS=( $($_comp_command1 help 2>/dev/null | \
    awk 'FNR > 2 {print $2}' | paste -s -d' '  -) )
  _arguments '1: :(${ARGS[@]})'

compdef _task task

It translate the one command per line output of help into an array of strings (ARGS), and sends it to the internal zsh autocomplete.

Final thoughts

task has completely changed how I self-document my projects. Doing a quick search on my computer, there are 43 task files. It makes it easier for me to go back to projects that I haven’t touched in a while.

More importantly, it’s extremely lightweight, it has no external dependencies, and it’s fairly accessible to anyone that knows a bit of bash.