Why I wrote my own task runner — twice. And why you should care!
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!