Skip to content

Taskfile

The standalone Makefile.

.forgejo/workflows/build.yml matrix-badge


The Taskfile is a simple bash script and interface for any software project.

It is to be understood as a software development practice to standardize the installation, configuration and execution of different software frameworks.

  • 🛠️ Installation: Setup and configure software and tools with ease.
  • 🔄 CI Pipelines: Simplify CI pipelines by making the executed task reproducible.
  • 📚 Library commands: A collection of reusable commands for common scripting tasks.

Specification

The specification of the Taskfile is a short guide to setup a Python project.

You start by creating a task file in your project.

touch task

Make it executable.

chmod +x task

Open it in an editor and add the bash shebang:

#!/usr/bin/env bash

This is the only dependency a Taskfile has.

Next append the abort on error setting:

set -e

The Taskfile generates a help table. You simply append new help entries and the help-table functions generates a beautiful table.

help-table() {
    local -a rows
    local max_cmd=7 max_opt=6 max_desc=11

    rows+=("Command|Option|Description")
    rows+=("help|[grep]|Show help for commands.")
    rows+=("install||Install build dependencies.")
    rows+=("lint||Run pre-commit.")
    rows+=("version||Show version of required tools.")

     for row in "${rows[@]}"; do
        IFS='|' read -r cmd opt desc <<< "$row"
        (( ${#cmd} > max_cmd )) && max_cmd=${#cmd}
        (( ${#opt} > max_opt )) && max_opt=${#opt}
        (( ${#desc} > max_desc )) && max_desc=${#desc}
    done

    printf '| %-*s | %-*s | %-*s |\n' \
        "$max_cmd" "Command" \
        "$max_opt" "Option" \
        "$max_desc" "Description"

    printf '|-%*s-|-%*s-|-%*s-|\n' \
        "$max_cmd" "$(printf '%*s' "$max_cmd" '' | tr ' ' '-')" \
        "$max_opt" "$(printf '%*s' "$max_opt" '' | tr ' ' '-')" \
        "$max_desc" "$(printf '%*s' "$max_desc" '' | tr ' ' '-')"

    local first_row=true
    for row in "${rows[@]:1}"; do
        IFS='|' read -r cmd opt desc <<< "$row"
        if [[ "$cmd" =~ ^===.*===$ ]]; then
            group_name="${cmd/===/}"
            group_name="${group_name/===/}"

            if [ "$first_row" = true ]; then
                first_row=false
            else
                printf '|-%*s-|-%*s-|-%*s-|\n' \
                    "$max_cmd" "$(printf '%*s' "$max_cmd" '' | tr ' ' '-')" \
                    "$max_opt" "$(printf '%*s' "$max_opt" '' | tr ' ' '-')" \
                    "$max_desc" "$(printf '%*s' "$max_desc" '' | tr ' ' '-')"
            fi

            local left_pad=$(( (max_cmd - ${#group_name}) / 2 ))
            local right_pad=$(( max_cmd - left_pad - ${#group_name} ))
            printf '| %*s%s%*s | %*s | %*s |\n' \
                "$left_pad" "" \
                "$group_name" \
                "$right_pad" "" \
                "$max_opt" "" \
                "$max_desc" ""

            printf '|-%*s-|-%*s-|-%*s-|\n' \
                "$max_cmd" "$(printf '%*s' "$max_cmd" '' | tr ' ' '-')" \
                "$max_opt" "$(printf '%*s' "$max_opt" '' | tr ' ' '-')" \
                "$max_desc" "$(printf '%*s' "$max_desc" '' | tr ' ' '-')"
        else
            first_row=false
            printf '| %-*s | %-*s | %-*s |\n' \
                "$max_cmd" "$cmd" \
                "$max_opt" "$opt" \
                "$max_desc" "$desc"
        fi
    done
}

The help table also supports separator.

The default command to show the table is help:

help() {
    local grep_pattern
    echo
    if [[ -n "$1" ]]; then
        grep_pattern="$1"
        help-table | grep -i "$grep_pattern" | column -t -s'|'
    else
        echo 'task <command> [options]'
        echo
        echo 'commands:'
        echo
        help-table
    fi
    echo
}

Until here the parts of the Taskfile look more or less the same.

Depending on the software project implement the required commands:

install() {
    echo 'Setup venv and install Python dependencies'
    uv venv
    source .venv/bin/activate
    uv pip install pre-commit
}

lint() {
    source .venv/bin/activate

    echo 'Run pre-commit'
    pre-commit run --all-file
}

version() {
    uv --version
}

Finish the Taskfile with a command switch case.

if [[ "$1" == *","* ]]; then
    # Split comma-separated commands
    IFS=',' read -ra COMMANDS <<< "$1"
    for cmd in "${COMMANDS[@]}"; do
        # Trim whitespace
        cmd=$(echo "$cmd" | xargs)
        if declare -f "$cmd" > /dev/null; then
            "$cmd"
        else
            echo "Unknown command: $1"
            help "$@"
            exit 1
        fi
    done
elif declare -f "$1" > /dev/null; then
    "$1" "${@:2}"
else
    case "$1" in
        all)
            install
            lint
            ;;
        *)
            echo "Unknown command: $1"
            help
            exit 1
            ;;
    esac
fi

We have covered the main parts of every Taskfile.

Now see usage on how to use the Taskfile.

Development

Checkout the development page for details.