task
Run any project.
The task file is a simple bash script and standardized interface for all software projects.
It is to be understood as a software development pattern to standardize the installation, configuration and execution of different software frameworks.
Specification
The specification is a short guide to setting up a task file for a Python project.
- Create a file
task
in your project.
touch task
- Ensure it is executable.
chmod +x task
- First add the bash shebang.
#!/bin/bash
- Then append the abort on error setting.
set -e
- Load environment variables from the
.env
file.
if [[ -a ".env" ]]; then
source .env
fi
- Add a help function.
function help() {
echo
echo "task <command> [options]"
echo
echo "commands:"
echo
# Define column widths
CMD_WIDTH=10
OPT_WIDTH=6
DESC_WIDTH=40
COLUMN="| %-${CMD_WIDTH}s | %-${OPT_WIDTH}s | %-${DESC_WIDTH}s |\n"
# Print table header
printf "$COLUMN" "Command" "Option" "Description"
echo "|$(printf '%*s' $((CMD_WIDTH + 2)) '' | tr ' ' '-')|$(printf '%*s' $((OPT_WIDTH + 2)) '' | tr ' ' '-')|$(printf '%*s' $((DESC_WIDTH + 2)) '' | tr ' ' '-')|"
# Print table rows
printf "$COLUMN" "all" "" "Run all tasks."
printf "$COLUMN" "install" "" "Setup the local environment."
printf "$COLUMN" "lint" "" "Run pre-commit and update index.html."
printf "$COLUMN" "source" "" "Source the Python virtual env."
printf "$COLUMN" "version" "" "Show version of required tools."
echo
}
- Setup the command functions.
function version() {
uv --version
}
function install() {
echo "Setup venv and install python dependencies"
uv venv env
source env/bin/activate
uv pip install pre-commit
}
function lint() {
source env/bin/activate
echo "Run pre-commit"
pre-commit run --all-file
}
- Finally finish the file with command switch cases.
if declare -f "$1" > /dev/null; then
"$1" "${@:2}"
else
case "$1" in
all)
install
lint
;;
source)
source env/bin/activate
;;
*)
echo "Unknown command: $1"
help
exit 1
;;
esac
fi
Naming
The naming of functions is important. There are basically two styles:
- Action + Object
- Object + Action
The task file function use the first style. The name of function starts with the action followed by the object. The object definition can be singular and/or plural.
Examples for actions: activate, install, dev, develop, init, build, start, update, remove, delete, enable, disable, template, convert, create, edit, change, get, set, patch, fetch, generate, push, pull, import, export, list, publish, release, test, setup, prepare, restart, stop, store, restore, translate, upgrade, zip, visualize, sync, switch, run, reset, load, dump, checkout, commit, drop, deploy, handle, trigger, render, lint, uninstall, split, parse, fix, refactor, transform, cat, ls, rm, serve, help, show, filter, login, logout, encrypt, decrypt, upload, download, analyse, transpile, compile, minify, copy
Examples for objects: env, venv, submodule, container, database, snippet, model, module, repo, mail, doc, dependency, view, user, vault, file, host, node, log, password, hash, script, requirement, part, component, system, workspace, image, process, state, platform, dir, folder, readme, overview, lang, level, request, response, result, worker, server, proxy, workflow, volume, network, package, field, value, secret, chart, node, edge, function, method, firewall, html, css, image, svg, style, query, native, group, notebook
Objects can be tools: odoo, vupress, nodejs, zsh, bash, fish, podman, kind, minikube, helm, nvim, docker, podman, rust, python, tmux, vim, helix, system, git, pass, llm, sql, dotenv, javascript, vue, vite, astro, typescript, turbo, pnpm, eslint, jenkins, k8s, nextcloud, postgres, metabase, ansible, prometheus, grafana, hugo, deno, bun, babel, panda, gulp, grunt, electron, react, express, mongodb, angular, ionic, meteor, webpack, bower, jupyter
Patterns
The task file showed above is very basic. Commands can have parameters and functions call each other. The following is a collection of more complex task file patterns.
Set default parameter
Fallback to a default value for a parameter.
function build() {
PLATFORM="amd64"
if [ -n "$1" ]; then
PLATFORM="$1"
fi
Ensure parameter is not empty
Check the first param and exit if it is empty.
function deploy() {
if test -z "$1"; then echo "\$1 is empty."; exit; fi
Prompt for input
Use read
to ask for inputs.
if [ -z "$2" ]; then
read -p "Enter the task description: " TASK_DESCRIPTION
else
TASK_DESCRIPTION="$2"
fi
Setup local env vars
Define env vars at the beginning of the task file.
CONFIGURATION_FILE="file.conf"
GIT_BRANCH=$(git symbolic-ref --short -q HEAD)
Template with env vars
Create a parameterized file from a template. Requires envsubst
.
function template-with-env() {
echo "Template $CONFIGURATION_FILE"
export CONFIGURATION_1
export CONFIGURATION_2=${CONFIGURATION_2:="value"}
envsubst < "file.conf.template" > "$CONFIGURATION_FILE"
}
Create Python virtual env
Initialize Python virtual env with uv.
function init-venv() {
if [ ! -d "venv$GIT_BRANCH" ]; then
echo "Init venv$GIT_BRANCH with $(uv --version)."
uv venv "venv$GIT_BRANCH"
fi
}
Activate Python virtual env
function activate-venv() {
echo "Source virtualenv venv$GIT_BRANCH."
source "venv$GIT_BRANCH/bin/activate"
echo "$(python --version) is active."
}
Call a Python script
Run a Python script.
function generate-password-hash() {
activate-venv
if test -z "$1"; then echo "\$1 is empty."; exit; fi
PASSWORD_PLAIN="$1"
scripts/password_hash
}
#!/usr/bin/env python3
import os
from passlib.context import CryptContext
crypt_context = CryptContext(schemes=['pbkdf2_sha512', 'plaintext'], deprecated=['plaintext'])
password = os.environ.get('PASSWORD_PLAIN')
print(crypt_context.hash(password))
Command with named parameters
Assuming you have a docker-compose.yml
and would like to start selected or all containers.
function start() {
if [[ "$1" =~ "db" ]]; then
docker compose up -d db
fi
if [[ "$1" =~ "admin" ]]; then
docker compose up -d admin
echo "Open http://localhost:8000 url in your browser."
fi
if [[ "$1" =~ "odoo" ]]; then
docker compose up -d odoo
echo "Open http://localhost:8069 url in your browser."
fi
if [[ "$1" =~ "mail" ]]; then
docker compose up -d mail
echo "Open http://localhost:8025 url in your browser."
fi
}
Start all containers with task start
and selected with task start db,admin
.
Run commands in container
Use docker exec -i
to run commands in a container.
function drop-db() {
DATABASE="$1"
if [ -z "$DATABASE" ]; then
DATABASE="example"
fi
docker exec -i db psql "postgres://odoo:odoo@localhost:5432/postgres" -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '$DATABASE';"
docker exec -i db psql "postgres://odoo:odoo@localhost:5432/postgres" -c "DROP DATABASE \"$DATABASE\";"
}
Loop over files and folders
Use for
to loop over file, folders or arrays.
function render() {
echo "Update index.html for all folders"
for FOLDER in ./*; do
if [ -f "$FOLDER/README.md" ]; then
cd "$FOLDER" || exit
md2html README.md _site/index.html
cd .. || exit
fi
done
}
Parse and reassemble a file
With this function you can split a file into multiple parts whenever a specific keyword is matching. In this example the keyword is !vault
.
function convert-vault-file() {
FILE_PATH="$1"
TEMP_FILE=$(mktemp)
TEMP_PART_FILE=$(mktemp)
WRITE_FINISHED=false
while IFS= read -r LINE; do
# Check for keyword
if [[ "$LINE" =~ "!vault" ]]; then
# Process part if ready to write
if [ "$WRITE_FINISHED" ] && [ -s "$TEMP_PART_FILE" ]; then
# Decrypt part file and write to assemble file
ansible-vault decrypt "$TEMP_PART_FILE"
KEY=$(echo "$LINE" | cut -d':' -f1)
VALUE=$(cat "$TEMP_PART_FILE")
echo "$KEY: $VALUE" >> "$TEMP_FILE"
fi
# Clear the file
: > "$TEMP_PART_FILE"
# Flag as ready to write
WRITE_FINISHED=true
else
if [ "$WRITE_FINISHED" ]; then
# Pipe into part file
echo "$LINE" >> "$TEMP_PART_FILE"
fi
fi
done < "$FILE_PATH"
# Output assembled file
cat "$TEMP_FILE"
# Cleanup temp files
rm -f "$TEMP_FILE"
rm -f "$TEMP_PART_FILE"
}
Usage
Running the task file requires a shell alias: alias task='./task'
Show the available commands with task help
.
From the specification the project can be installed with task install
.
To source the Python environment run source task source
.
Execute all commands with task all
.
Completion
With the output of task help
the task commands can be completed.
Completion for bash: /etc/bash_completion.d/task_completions
#!/bin/bash
_task_completions() {
local cur prev commands
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
commands=$(./task help | grep -A999 -e '---' | awk '{print $2}' | sed 's/^[[:space:]]*//' | grep -v '^$' | tr '\n' ' ')
if [[ ${COMP_CWORD} == 1 ]]; then
COMPREPLY=( $(compgen -W "${commands}" -- "${cur}") )
elif [[ ${COMP_CWORD} == 2 ]]; then
COMPREPLY=( $(compgen -f -- "${cur}") )
else
COMPREPLY=()
fi
}
complete -F _task_completions task
Completion for zsh: ~/.oh-my-zsh/completions/_task
#compdef task
_arguments '1: :->tasks' '*: :_files'
case "$state" in
tasks)
args=$(./task help | grep -A999 -e '---' | awk '{print $2}' | sed 's/^[[:space:]]*//' | grep -v '^$' | tr '\n' ' ')
args="$args help"
_arguments "1:profiles:($args)"
;;
esac
GitHub Actions
Running task file commands in GitHub Actions is highly recommended. This way you can run the same CI/CD procedures in the GitHub runner as you do on your localhost.
The GitHub Actions config is simple: .github/workflows/test.yml
on:
pull_request:
branches:
- "main"
push:
branches:
- "main"
jobs:
task-all:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Run task install
run: ./task install
- name: Run task lint
run: ./task lint
Jenkins
Run task file commands in Jenkins: Jenkinsfile
pipeline {
agent any
stages {
stage('version') {
steps {
script {
currentBuild.description = sh (script: 'git log -1 --pretty=%B', returnStdout: true).trim()
}
sh './task version'
}
}
stage('install') {
steps {
sh './task install'
}
}
stage('lint') {
steps {
sh './task lint'
}
}
}
}%
Example
This website is built with a task file. Here is the source:
#!/bin/bash
set -e
function help() {
echo
echo "task <command> [options]"
echo
echo "commands:"
echo
# Define column widths
CMD_WIDTH=10
OPT_WIDTH=6
DESC_WIDTH=40
COLUMN="| %-${CMD_WIDTH}s | %-${OPT_WIDTH}s | %-${DESC_WIDTH}s |\n"
# Print table header
printf "$COLUMN" "Command" "Option" "Description"
echo "|$(printf '%*s' $((CMD_WIDTH + 2)) '' | tr ' ' '-')|$(printf '%*s' $((OPT_WIDTH + 2)) '' | tr ' ' '-')|$(printf '%*s' $((DESC_WIDTH + 2)) '' | tr ' ' '-')|"
# Print table rows
printf "$COLUMN" "all" "" "Run all tasks."
printf "$COLUMN" "install" "" "Install node packages."
printf "$COLUMN" "dev" "" "Run 11ty server."
printf "$COLUMN" "build" "" "Build the 11ty website."
printf "$COLUMN" "serve" "" "Serve the 11ty output folder."
printf "$COLUMN" "version" "" "Show version of required tools."
echo
}
function install() {
npm install
}
function dev() {
npx eleventy --serve
}
function build() {
npx eleventy
}
function serve() {
npx serve ./_site
}
function version() {
echo "eleventy: $(npx eleventy --version)"
}
if declare -f "$1" > /dev/null; then
"$1" "${@:2}"
else
case "$1" in
all)
install
build
;;
*)
echo "Unknown command: $1"
help
exit 1
;;
esac
fi
Source: janikvonrotz/taskfile.build
More examples
Implementations of the task file standard can be accessed from these projects: