--- layout: layouts/base.njk permalink: / ---

./task

Run any project.

![Vercel](https://vercelbadge.vercel.app/api/janikvonrotz/taskfile.build) --- 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. ```bash touch task ``` - Ensure it is executable. ```bash chmod +x task ``` - First add the bash shebang. ```bash #!/bin/bash ``` - Then append the abort on error setting. ```bash set -e ``` - Load environment variables from the `.env` file. ```bash if [[ -a ".env" ]]; then source .env fi ``` - Add a help function. ```bash function help() { echo echo "task [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. ```bash 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. ```bash 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: 1. Action + Object 2. 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. ```bash 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. ```bash function deploy() { if test -z "$1"; then echo "\$1 is empty. Usage:"; help | grep "${FUNCNAME[0]}" exit 1; fi ``` ### Prompt for input Use `read` to ask for inputs. ```bash 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. ```bash 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`. ```bash 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. ```bash 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 ```bash 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. ```bash function generate-password-hash() { activate-venv if test -z "$1"; then echo "\$1 is empty."; exit; fi PASSWORD_PLAIN="$1" bin/password-hash } ``` ```python #!/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. ```bash 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. ```bash 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. ```bash 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 convert 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`. ```bash 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" } ``` ### Process data This pattern tries to match the worklow of a jupyter notebook. Create a folder with the data processing scripts: ``` scripts ├── 01_import-mail-data ├── 02_transform-mail-data └── 03_export-mail-data ``` The following function lists the scripts and asks for the number of scripts to run. Every script until the entered number is concatenated and executed. ```bash function process-data() { while true; do SCRIPTS=($(ls scripts)) echo -e "Available scripts:\n" for INDEX in "${!SCRIPTS[@]}"; do echo "$((INDEX+1)): ${SCRIPTS[$INDEX]}" done echo "" read -p "Enter the number of scripts to run (or 'q' to quit): " USER_INPUT if [ "${USER_INPUT}" = "q" ]; then break fi TEMP_FILE=$(mktemp) for INDEX in "${!SCRIPTS[@]}"; do if [ $USER_INPUT -ge $((INDEX+1)) ]; then cat "scripts/${SCRIPTS[$INDEX]}" >> "$TEMP_FILE" echo >> "$TEMP_FILE" fi done clear echo -e "\nPython Output:\n\n---" python "$TEMP_FILE" EXIT_CODE=$? echo -e "---\n" echo -e "Exit code: $EXIT_CODE\n" rm "$TEMP_FILE" done } ``` ## 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` ```bash #!/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` ```bash #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 ` ```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` ```groovy 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: ```bash #!/bin/bash set -e function help() { echo echo "task [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](https://github.com/janikvonrotz/taskfile.build/blob/main/task) ### More examples Implementations of the task file standard can be accessed from these projects: - [janikvonrotz/dotfiles](https://github.com/janikvonrotz/dotfiles/blob/main/task) - [janikvonrotz/python.casa](https://github.com/janikvonrotz/python.casa/blob/main/task) - [Mint-System/Ansible-Build](https://github.com/Mint-System/Ansible-Build/blob/main/task) - [Mint-System/Odoo-Build](https://github.com/Mint-System/Odoo-Build/blob/16.0/task) - [Mint-System/Odoo-Wiki](https://github.com/Mint-Wiki/Odoo-Build/blob/main/task) - [Mint-System/Website](https://github.com/Mint-System/Website/blob/main/task) - [Mint-System/Wiki](https://github.com/Mint-System/Wiki/blob/main/task)