Janik von Rotz cf57626dbc docs(agents): reorder variable naming guideline and clarify conditional syntax in bash style guide
- Reorder local variable naming rule in AGENTS.md for better flow
- Add clarification note about escaping brackets when replacing `[ ]` with `[[ ]]`
- Emphasize that `[]` acts as a regex filter in conditional expressions
2025-10-29 14:53:03 +01:00
2025-04-11 08:25:06 +02:00
2025-04-11 08:25:06 +02:00
2025-04-08 15:21:25 +02:00

layout, permalink
layout permalink
layouts/base.njk /

./task

Run any project.

Vercel


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 task in 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 .env file.
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:

  1. Action + Object
  2. 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:

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."
S
Description
No description provided
Readme AGPL-3.0
1.3 MiB
0 Stars 1 Watchers 0 Forks
Languages
Shell 99.5%
Dockerfile 0.5%