Why I wrote my own task runner — twice. And why you should care!

David Danier
5 min readMar 10, 2024

--

Some years back I figured that the many projects I have always lack some common base to work on those. What I mean by that is some common command you can run to install the project, update the project and run the project. As I am mostly doing web development with different tools (FastAPI, Django, maybe node, sometimes PHP) most project were somewhat different. And even the project based on the same technology tended to diverge over time.

It was at this time I figured I wanted to build a task runner, to run those common tasks like “install” without the need to know what actually is happening during install. As this was still 2017 no ready to use task runners did exist, so I created my own one.

My first task runner

It was based on the idea of using simple shell scripts to execute the different tasks. The simplified version did look like this:

task:example() {
echo "example task"
}

With this in mind I created b5 a task runner based on Python that builds a bash script using a Taskfile and then allowed you to run any of the bash functions beginning with "task:" . You could for example use b5 example to run the example task shown above.

And having this was great. We built all our projects around this concept and are still using b5 as our main task runner for all of our projects.

Why writing b5 tasks can be hard

But then there is bash and bash may be great for many things — but it also has its drawbacks. After some time we needed to add interactive elements using read or added some more parameters to certain tasks (b5 task --some parameter ). All of this produces a lot of code if done in pure bash.

Along came just

Then just was released. It solved many of the problems I had with writing bash scripts. For example just did parse command line arguments for you and ensured all required arguments were actually there. Also it simplified how what a task runner should actually do, at least for me.

But then just also wasn’t perfect. The fact it runs every line in its own shell is an issue for me, as we were used to have multiple bash functions as helpers in our Taskfile ‘s. And having very small reusable tasks that constantly call each others is not that nice to write (see the justfile of one of our Python libraries for example).

What just was missing for me was always the possibility to really write some code and not just execute some commands. I know you can execute whole tasks using for example bash, but then why not just stick to b5 ?

For a long time I thought to solve this I needed to implement my own scripting language that kind of combines bash with the way just defines the task parameters, but then…

A new shell — the nu shell

Some month ago I discovered the nu shell. After reading through the docs I switched my system shell over to nu . This shell solves many of the most annoying issues I always had when writing bash code. Look for example how you can define your own custom commands also accepting some parameters:

def my-command [
name: string
--age: int
] {
# run some code
}

This looks a LOT like what I always wanted b5 to become after knowing how just handled the arguments. In addition arguments allow to be typed, which is also true for any variable in nu .

Besides that nu also fixes many of the issues you may encouter while piping text data from one cli tool to another. nu tried to use structured data everywhere so something like ls | where size > 10mb | sort-by modified just works out of the box, no need to parse the text data yourself and hope for the best.

My second task runner — nur

With all those nu features I started to like in my default shall I thought “Could I convert b5 to be using nu scripting instead of bash?”

Instead I created a completely new task runner called nur, using the nu libraries (crates) and written in rust . This allowed me to be even more flexible about what nur can do and how to accomplish it.

Simple example nurfile:

def "nur example" [] {
print "example task"
}

You could then call nur example and execute the task. Also — as all of this is happening in nu script — you can just use all of the many nu commands, the same way you could use those in your nu shell. You can also define your own helper commands to be used by your nur tasks, you can even use modules to structure those helper commands.

The best thing is that nur does not need you to have nu installed. This is important as b5 always had the issue that everybody needed to have bash installed, this was always an issue, especially on Windows.

Note: nur is still an early version. Not everything is perfect yet, still if works pretty stable for me.

Why is it important to have a task runner

After having used a task runner for over 7 years now I can say one thing for sure: Projects without a task runner and some well defined tasks are a hassle to work with.

For me this means that I start adding a Taskfile or nowadays a nurfile to projects I work with — that do not have a task runner yet. If you take working with your colleagues serious you should really care about having a task runner in your project setup.

Also it is really necessary to have a common set of tasks, for example we use:

  • “install” → setup the project, install all dependencies
  • “update” → ensure everything is up to date
  • “run” → start the project, will normally run a webserver in our case
  • “halt” → stop the running project
  • “test” -> run the tests
  • “lint” → run the linter

This way I don’t need to care about what linter is used for example, it just works.

Conclusion

You should really have a task runner in any of your projects. When working with others just might be a good choice — but I would very much like if you would also try my new flavour of a task runner called nur .

Happy coding!

--

--

David Danier

Python Web Developer from Germany, focusing on FastAPI and Django.