layout, permalink
| layout | permalink |
|---|---|
| layouts/base.njk | / |
./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 set up a task file for a Python project.
- Create a file
taskin your project.
touch task
- Ensure it is executable.
chmod +x task
- First add the bash shebang.
#!/usr/bin/env bash
- Then append the abort on error setting.
set -e
- Load environment variables from the
.envfile.
if [[ -a ".env" ]]; then
source .env
fi
- Add a help function.
help-table() {
local cmd_width=10
local opt_width=6
local desc_width=40
local column="| %-${cmd_width}s | %-${opt_width}s | %-${desc_width}s |\n"
printf "$column" "Command" "Option" "Description"
echo "|$(printf '%*s' $((cmd_width + 2)) '' | tr ' ' '-')|$(printf '%*s' $((opt_width + 2)) '' | tr ' ' '-')|$(printf '%*s' $((desc_width + 2)) '' | tr ' ' '-')|"
printf "$column" "help" "[grep]" "Show help for commands."
printf "$column" "install" "" "Setup the local environment."
printf "$column" "lint" "" "Run pre-commit and update index.html."
printf "$column" "version" "" "Show version of required tools."
}
help() {
echo
if [[ -n "$1" ]]; then
help-table | grep -i "$1" | column -t -s'|'
else
echo "task <command> [options]"
echo
echo "commands:"
echo
help-table
fi
echo
}
- Setup the command functions.
version() {
uv --version
}
install() {
echo "Setup venv and install python dependencies"
uv venv env
source env/bin/activate
uv pip install pre-commit
}
lint() {
source env/bin/activate
echo "Run pre-commit"
pre-commit run --all-file
}
- Finish the task file with command switch cases.
if declare -f "$1" > /dev/null; then
"$1" "${@:2}"
else
case "$1" in
all)
install
lint
;;
*)
echo "Unknown command: $1"
help
exit 1
;;
esac
fi
These are the main parts of every task file script.
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.
Execute all commands with task all.
Image
See taskfile.build/image for details.
Naming
The naming of functions is important. There are basically two styles:
- Action + Object
- Object + Action
The task file functions use the first style. The name of the function starts with an action followed by an object. The object name can be singular 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
Generate Function Names
Style
The bash scripts follow the Bash Style Guide | ysap.sh. Have a look at the AGENTS.md for details on the applied styling.
Patterns
The task file showed above is very basic. Commands can have parameters and 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.
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.
deploy() {
if [[ -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.
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.
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
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.
generate-password-hash() {
activate-venv
if [[ -z "$1" ]]; then echo "\$1 is empty."; exit; fi
PASSWORD_PLAIN="$1"
bin/password-hash
}
The Python script: bin/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.
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.
drop-db() {
local 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.
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.
convert-vault-file() {
local file_path="$1"
local temp_file=$(mktemp)
local temp_part_file=$(mktemp)
local write_finished=false
local current_key=""
while IFS= read -r line; do
# Check for keyword
if [[ "$line" =~ "!vault" ]]; then
# Process previous vault entry if it exists
if [[ "$write_finished" = true ]] && [[ -n "$current_key" ]] && [[ -s "$temp_part_file" ]]; then
# Decrypt part file and write to assemble file
ansible-vault decrypt "$temp_part_file"
local value=$(cat "$temp_part_file")
echo "$current_key: $value" >> "$temp_file"
fi
# Set up for new vault entry
current_key=$(echo "$line" | cut -d':' -f1)
# Clear the file
: > "$temp_part_file"
# Flag as ready to write
write_finished=true
else
if [[ "$write_finished" = true ]]; then
# Pipe into part file
echo "$line" >> "$temp_part_file"
fi
done
done < "$file_path"
# Process the final vault entry
if [[ "$write_finished" = true ]] && [[ -n "$current_key" ]] && [[ -s "$temp_part_file" ]]; then
ansible-vault decrypt "$temp_part_file"
local value=$(cat "$temp_part_file")
echo "$current_key: $value" >> "$temp_file"
fi
# 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.
process-data() {
while true; do
local 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
local temp_file=$(mktemp)
for index in "${!scripts[@]}"; do
if (( $user_input >= $((index+1)) )); then
cat "scripts/${scripts[$index]}" >> "$temp_file"
echo >> "$temp_file"
fi
done
clear
echo -e "\nPython Output:\n\n---"
python "$temp_file"
local exit_code=$?
echo -e "---\n"
echo -e "Exit code: $exit_code\n"
rm "$temp_file"
done
}
Integration
The task file is intended to be integrated into the shell setup. Where applications provides a domain specific language (DSL) ensure that the DSL only calls the task file.
Completion
With the output of task help the task commands can be completed.
Completion for bash: /etc/bash_completion.d/task_completions
#!/usr/bin/env 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
complete -F _task_completions t
Completion for zsh: ~/.oh-my-zsh/completions/_task
#compdef task t
_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
The completion also works for task alias alias t='./task'.
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: Install
run: ./task install
- name: 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:
#!/usr/bin/env bash
set -e
help-table() {
cmd_width=20
local opt_width=6
desc_width=42
column="| %-${cmd_width}s | %-${opt_width}s | %-${desc_width}s |\n"
printf "$column" "Command" "Option" "Description"
echo "|$(printf '%*s' $((cmd_width + 2)) '' | tr ' ' '-')|$(printf '%*s' $((opt_width + 2)) '' | tr ' ' '-')|$(printf '%*s' $((desc_width + 2)) '' | tr ' ' '-')|"
printf "$column" "all" "" "Run all tasks."
printf "$column" "build-11ty" "" "Build the 11ty website."
printf "$column" "build-image" "" "Build Docker image."
printf "$column" "commit-with-llm" "" "Commit with llm generated commit message."
printf "$column" "dev-11ty" "" "Run 11ty server."
printf "$column" "help" "[grep]" "Show help for commands."
printf "$column" "install" "" "Install node packages."
printf "$column" "lint" "" "Lint code with prettier."
printf "$column" "run" "" "Run Docker image."
printf "$column" "run-tests" "" "Test with shellcheck."
printf "$column" "serve-11tyy" "" "Serve the 11ty output folder."
printf "$column" "version" "" "Show version of required tools."
}
help() {
echo
if [[ -n "$1" ]]; then
help-table | grep -i "$1" | column -t -s'|'
else
echo "task <command> [options]"
echo
echo "commands:"
echo
help-table
fi
echo
}
# Static vars
BUILDX_PLATFORM="linux/amd64"
CONTAINER_REGISTRY="janikvonrotz"
CONTAINER_TAG="taskfile.build:latest"
DOCKERFILE="image/Dockerfile"
# Import commands
if [[ -d "$HOME/taskfile.build/bin" ]]; then
for file in "$HOME/taskfile.build/bin/"*; do
if [[ -f "$file" ]]; then
source "$file"
fi
done
fi
# Project commands
run() {
echo "Open http://localhost:8080 in your browser"
docker run -p 8080:80 \
-e GIT_URL="https://git.taskfile.build" \
-e TASK_COMMANDS="install,build-11ty" \
-e CADDY_ROOT="_site" \
janikvonrotz/taskfile.build:latest
}
install() {
pnpm install
}
dev-11ty() {
pnpm dlx @11ty/eleventy --serve
}
lint() {
pnpm exec prettier . --write
}
run-tests() {
echo "Test with shellcheck."
shellcheck task
}
build-11ty() {
pnpm dlx @11ty/eleventy
}
serve-11ty() {
pnpx serve ./_site
}
version() {
echo "eleventy: $(pnpm dlx @11ty/eleventy --version)"
}
if declare -f "$1" > /dev/null; then
"$1" "${@:2}"
else
case "$1" in
all)
install
build-11ty
;;
*)
echo "Unknown command: $1"
help
exit 1
;;
esac
fi
Source: janikvonrotz/taskfile.build
More examples
More links to the implementations of the task file standard:
- janikvonrotz/dotfiles
- janikvonrotz/python.casa
- Mint-System/Ansible-Build
- Mint-System/Kubernetes-Build
- Mint-System/Odoo-Build
- Mint-System/Odoo-Wiki
- Mint-System/Website
- Mint-System/Wiki
Library
The repository of this website provides a library of reusable functions.
Clone the repository into your home folder and ensure it is updated regurarly.
clone-taskfile(){
if [[ ! -d "$HOME/taskfile.build" ]]; then
echo -e "\033[38;5;214mGit\033[0m: Clone taskfile repo"
git clone https://git.taskfile.build "$HOME/taskfile.build"
else
echo -e "\033[38;5;214mGit\033[0m: Pull taskfile repo"
git -C "$HOME/taskfile.build" pull
fi
}
In your task file you can import these functions from the taskfile library.
if [[ -d "$HOME/taskfile.build/bin" ]; then
for file in "$HOME/taskfile.build/bin/"*; do
if [[ -f "$file" ]]; then
source "$file"
fi
done
fi
Task
For autocomplete add the command to the help table.
init-config-dir
printf "$column" "init-config-dir" "" "Setup task file config dir."
template-dotenv
printf "$column" "template-dotenv" "" "Generate .env from .env.template."
Python
Set git env vars.
GIT_BRANCH=$(git symbolic-ref --short -q HEAD)
init-venv
printf "$column" "init-venv" "" "Initialize and activate Python virtual env."
Environment
create-odoo-env
printf "$column" "create-odoo-env" "[env]" "Create env file for Odoo instance."
create-nextcloud-env
printf "$column" "create-nextcloud-env" "[env]" "Create env file for Nextcloud Instance."
list-env
printf "$column" "list-env" "" "List env files."
show-env
printf "$column" "show-env" "[env]" "Output content of the env file."
copy-env
printf "$column" "copy-env" "[env][env]" "Copy env file."
edit-env
printf "$column" "edit-env" "[env]" "Open env file in default editor."
load-env
printf "$column" "load-env" "[env]" "Load and export env file."
rename-env
printf "$column" "rename-env" "[env][env]" "Rename env file."
remove-env
printf "$column" "remove-env" "[env]" "Remove environment config."
backup-env-files
printf "$column" "backup-env-files" "[path]" "Archive and copy env files to target location."
restore-env-files
printf "$column" "restore-env-files" "[path]" "Extract and copy env files from backup file."
LLM
commit-with-llm
printf "$column" "commit-with-llm" "" "Commit with llm generated commit message."
update-with-llm
printf "$column" "update-with-llm" "[glob][prompt]" "Feed files matching glob to LLM and apply changes."
Git
update-submodule
printf "$column" "update-submodule" "" "Update git submodules."
Pass
Set path to pass entry.
PASS_ENTRY=$(echo "env/${PWD##*/}" | tr '[:upper:]' '[:lower:]')
save-dotenv-to-pass
printf "$column" "save-dotenv-to-pass" "" "Store content of .env in pass entry."
load-dotenv=from-pass
printf "$column" "load-dotenv=from-pass" "" "Restore content to .env from pass entry."
KeePass
Set path to KeePass database and entry.
KEEPASS_DATABASE_FILE="/path/to/database.kdbx"
KEEPASS_DATABASE_KEY_FILE="/path/to/keepass.key"
KEEPASS_ENTRY=$(echo "env/${PWD##*/}" | tr '[:upper:]' '[:lower:]')
save-dotenv-to-keepass
printf "$column" "save-dotenv-to-keepass" "" "Store content of .env in KeePass entry."
load-dotenv-from-keepass
printf "$column" "load-dotenv-from-keepass" "" "Restore content to .env from KeePass entry."
Docker
Set Docker env vars.
BUILDX_PLATFORM="linux/amd64"
CONTAINER_REGISTRY="janikvonrotz"
CONTAINER_TAG="python.casa:latest"
DOCKERFILE="image/Dockerfile"
DOCKER_CONFIG="$HOME/.docker/$CONTAINER_REGISTRY/"
login-docker
printf "$column" "login-docker" "[user][token]" "Run Docker login and create docker config."
build-image
printf "$column" "build-image" "" "Build Docker image with buildx."