Task organisation for dev projects,
based on a pure shell script.
Get started
Download
(Latest version of task runner)
- Download task runner
- Make it executable, e.g. chmod +x run
- Put into path, e.g. mv run /usr/local/bin/run
Needs Bash 3.2+, tested on Linux and macOS.
How it works
Place a task file called run.sh in your project root and make it executable. Let’s say your project is a then your task file could look like this:
run::compile() {
go build \
-o out/ \
src/main.go
}
run::install() {
go get -t ./...
go mod tidy
}
run::bundle() {
./node_modules/.bin/esbuild \
src/index.ts \
--bundle \
--outfile=dist/app.js
}
run::install() {
npm install --strict-peer-deps
}
run::server() {
PORT=8000 \
CONFIG=/app/config.yml \
python src/server.py
}
run::install() {
pip install \
-r requirements.txt
}
A run.sh task file is a plain regular shell script. It behaves exactly as you would expect from any other shell script. There is no special magic to it, except that it adheres to the following convention:
- The task definitions are shell commands (functions) that carry a run:: prefix.
- Tasks can optionally be preceded by a descriptive comment. The first comment line is interpreted as title, and the following lines as additional info text.
These notation rules allow the run task runner to recognise and process the tasks.
Run a task
For example, if you want to execute the install task:
$ run install
Under the hood, the task runner evaluates the task file in a bash subprocess and then invokes the task with the respective name – in this case, the bash function run::install.
Any additional CLI arguments will be passed on to the task as is.
List all available tasks
List all available tasks along with their title:
$ run --list
If a task has additional lines of commentary below the title, you can print that by running e.g. run --info install.
Usage without task runner
Without the task runner available (e.g. when in a CI or production environment), you can still access your tasks by sourcing the run.sh task file and then directly calling the task commands by their original names.
$ . run.sh
$ run::install
(The task runner effectively does the same thing.)
Why?
- Provide a set of well-groomed and well-defined tasks as entrypoints to your project – think of it as friendly API for developers to interact with your project.
- The run.sh file structure is simple, discoverable, and self-documenting. It’s also agnostic of the project’s main language.
- Re-use the very same command configurations on all stages, from development over CI to production.
- What could be more straightforward than storing shell commands in shell scripts? Shell script might not be the nicest language on earth, but it’s powerful and widely adopted. (And when you work on the command line, you are knee-deep into shell script land anyway.)
- The task runner tool is optional: you neither force it onto everyone else, nor do you have to roll it out to all stages.
Shell Cheat Sheet
Here are a few handy shell scripting snippets.
Variable with default fallback value
“Ternary” assignment
PORT=$([[ "$IS_PROD" ]] && echo 443 || echo 8080)
Task input args
run::hello() {
echo "$1"
echo "$2"
}
Pass on all task input args
run::print() {
echo "$@"
}
Call other task
run::hello() {
run::other-task
}
Check, if…
[[ -f file.txt ]]
[[ -d folder/ ]]
[[ -z "$VAR" ]]
[[ -n "$VAR" ]]
Read file contents into variable
Include other shell file
(The path is relative to the shell’s current working directory.)
Get absolute path of script’s location
THIS_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)
(This declaration must be placed at top level.)
Use .env files
source .env
[[ -f .env ]] && source .env
[[ -f .env ]] && source .env || source .env.dev
.png)


