Ein Task-Runner im Geiste von Make, aber ohne die Altlasten: YAML statt Tab-Fallen, ein einzelnes Binary statt Unix-Abhängigkeiten, und dasselbe Taskfile auf Linux, macOS und Windows.
1. Einleitung: Das Makefile-Problem
Wer jahrelang mit Makefiles gearbeitet hat, kennt ihre Stärken. Ein make build, ein make test, ein make deploy – schnell sind die Befehle ausgeführt, die sich niemand merken kann und die auch niemand von Hand eintippen möchte. Hinter den Befehlen steckt eine deklarative Beschreibung dessen, was getan werden soll, nicht wie man es manuell eintippt. Make ist schnell, überall vorhanden (zumindest auf Unix) und nach fast 50 Jahren noch immer der De-facto-Standard für Build-Prozesse.
Ich bin vor ein paar Jahren arbeitsbedingt von Mac auf Windows gewechselt. 😒 Make installieren, dachte ich, das dauert zehn Minuten. Zwei Stunden und diverse Anläufe später lief es – nur wusste ich ehrlich gesagt nicht mehr, welche der Versuche und Befehle mich zum Ziel gebracht haben. Die eigentliche Erkenntnis war eine andere: Wenn ich hier schon kämpfe, will ich das meinen Kollegen, die ebenfalls auf Windows arbeiten, nicht antun.
Die Installation war nur der erste Teil des Problems. Denn selbst wenn Make einmal läuft, warten unter Windows drei Stolperfallen. Windows bringt keinen Make-Interpreter mit – man muss MinGW, Cygwin oder ein separates GnuWin32-Paket installieren, nur um ein Skript auszuführen. Und selbst dann wäre das Makefile vermutlich kaputt, denn es verlässt sich stillschweigend auf sh, rm, cp, mkdir -p, grep und Co. Unter cmd.exe oder PowerShell funktioniert dieselbe Datei schlicht nicht.
Dazu kommt die berüchtigte Tab-Falle. Ein Makefile mischt Tabs und Spaces, und ein einziges falsch eingerücktes Leerzeichen erzeugt die Fehlermeldung „missing separator". Mit einem modernen Editor, der Spaces einsetzt, ist das ein ständiger Stolperstein. Und wer die Falle einmal verstanden hat, steht vor dem nächsten Problem: Variablen mit $(VAR) oder ${VAR}, automatische Variablen wie $@, implizite Regeln, PHONY-Deklarationen. Die Lernkurve ist steil, das Ergebnis schwer wartbar.
Die Folge: Genau die Automatisierung, die einem auf Linux oder Mac den Alltag erleichtert, fällt unter Windows weg. Man tippt Befehle wieder von Hand. Oder man pflegt zwei parallele Skriptsysteme, eines für Unix, eines für Windows.
Task löst genau dieses Problem. Der Task-Runner übernimmt das bewährte Make-Prinzip und wirft die Altlasten über Bord.
2. Was ist Task?
Task ist ein Task-Runner und Build-Werkzeug im Stil von Make. Der entscheidende Unterschied: Statt einer eigenen, fehleranfälligen Skriptsprache nutzt Task YAML als Beschreibungsformat.
Die wichtigsten Eigenschaften im Überblick:
| Eigenschaft | Beschreibung |
|---|---|
| YAML-basiert | Lesbare, klare Struktur statt Make-Syntax. Keine Tab/Whitespace-Probleme. |
| Single Binary | Ein einziges ausführbares Programm, keine Abhängigkeiten, keine Laufzeit-Installation. |
| Cross-Platform | Dasselbe Taskfile.yml läuft identisch auf Linux, macOS und Windows. |
| Modularität | Umfangreiche Projekte lassen sich in mehrere kleinere Taskfiles aufteilen und einbinden. |
| Make-inspiriert | Konzepte wie Dependencies, Variablen und Caching sind vertraut. |
| Erweiterte Features | Zusätzlich zu Make: --watch, parallele Ausführung, Preconditions, dynamische Variablen und mehr. |
Ein erster Blick auf eine minimale Task-Datei zeigt, worum es geht:
version: '3'
tasks:
build:
cmds:
- npm run buildVerglichen mit einem äquivalenten Makefile:
.PHONY: build
build:
npm run buildBeide tun dasselbe. Aber die YAML-Variante ist selbsterklärender, hat keine versteckten Regeln und funktioniert überall, ohne dass man sich Gedanken darüber machen muss, ob make überhaupt installiert ist.
Task ist übrigens kein Nischenprojekt: Unter anderem Docker, Vercel, HashiCorp, Microsoft, Google Cloud, AWS, Anthropic, MongoDB und GoReleaser setzen es ein. Wer Task verwendet, befindet sich in guter Gesellschaft – inklusive meiner Make-geplagten Vergangenheit.
3. Warum Task statt Make?
3.1 Cross-Platform ohne Zusatz-Setup
Das ist für Windows-Nutzer der größte Gewinn. Task bringt sein eigenes Shell-Handling mit und abstrahiert die Plattformunterschiede:
- Auf Linux/macOS führt Task Befehle standardmäßig mit
shaus. - Auf Windows wählt Task automatisch eine passende Shell (
cmdoder PowerShell) – oder du legst selbst fest, welche Shell verwendet werden soll (mehr dazu in Abschnitt 6.5).
Damit entfällt die klassische Frage „Läuft mein Makefile unter Windows?". Ein Taskfile.yml läuft überall. Vorausgesetzt, die aufgerufenen Programme selbst sind installiert – ein npm oder docker muss natürlich auch unter Windows vorhanden sein.
3.2 Keine Tab-/Whitespace-Fallen
YAML ist da eigenwillig, aber ehrlich: Einrückung ist auch hier wichtig, nur eben konsistent (Leerzeichen, üblicherweise zwei pro Ebene) und von jedem modernen Editor korrekt unterstützt. Die berüchtigte Make-Fehlermeldung „missing separator" existiert bei Task nicht.
3.3 Eingebaute, moderne Features
Task geht über Make hinaus, ohne dass man Zusatzwerkzeuge oder Make-Hacks benötigt: Dependencies (deps) lassen einen Task automatisch zuerst andere Tasks ausführen. Variablen (vars) und Umgebungsvariablen (env) kennt man aus Make, aber lesbarer. Caching über sources/generates baut nur neu, wenn sich Quelldateien geändert haben. Mit --watch überwacht Task Dateien und startet Tasks bei Änderungen erneut. Preconditions führen Tasks nur aus, wenn bestimmte Bedingungen erfüllt sind, und parallele Ausführung lässt mehrere Tasks gleichzeitig laufen. Und silent: unterdrückt die Echo-Ausgabe der Befehle, wie man sie von Make kennt, standardmäßig.
3.4 Lesbarkeit & Wartbarkeit
YAML ist für die meisten Entwickler:innen intuitiv lesbar, gerade auch für die, die nicht täglich mit Make arbeiten. Das senkt die Einstiegshürde fürs Team erheblich: Jeder kann eine Taskfile.yml lesen und verstehen, auch ohne Make-Erfahrung.
4. Wofür man Task verwenden kann
Task ist bewusst generisch. Es ist weder an eine Programmiersprache noch an einen bestimmten Workflow gebunden. Typische Einsatzgebiete:
Build & Compile
Der klassische Make-Anwendungsfall. Installieren, bündeln, transpilieren, Artefakte erzeugen – mit Caching, sodass nur tatsächlich veränderte Teile neu gebaut werden.
Code-Generierung & Scaffolding
Viele Projekte erzeugen Teile ihres Codes automatisch: Datenbank-Schemata, die in Typdefinitionen übersetzt werden, API-Beschreibungen, aus denen Clients generiert werden, oder Konfigurationsdateien, die aus Templates entstehen. Die Werkzeuge dafür sind verschieden, das Muster ist immer gleich: Eine Quelldatei ändert sich, ein Generator muss laufen, die Ergebnisse müssen an die richtige Stelle. Mit Task legst du diesen Ablauf einmal als Task an und startest ihn mit einem Befehl, statt die Generator-Befehle jedes Mal aus der Doku zu kramen.
Linter, Formatter & Qualitäts-Pipeline
Eine Tooling-Kette (format, lint, test, check) als Folge abhängiger Tasks bündeln – der Lesbarkeit wegen und damit der ganze Vorgang mit einem einzigen Befehl läuft.
Dev-Server & lokale Entwicklung
Server starten, Hot-Reload per --watch oder lokale Umgebungen mit docker compose steuern.
Docker, Deployment & CI
Docker-Images bauen, Container starten, Releases erstellen. Da das Taskfile identisch auf lokaler Maschine und in der CI läuft, vermeidest du die klassische Diskrepanz „lokal geht's, in der CI nicht".
Kurz gesagt
Überall dort, wo du wiederkehrende Befehlsfolgen bisher in make, in Shell-Skripten oder in package.json-Scripts ("scripts") abgelegt hast, ist Task eine saubere, sprach- und plattformunabhängige Alternative.
5. Installation
Task ist ein einzelnes Binary. Es gibt mehrere Wege – hier die gängigsten für Windows, Mac und Linux:
Windows: über Scoop (empfohlen für Entwickler)
scoop install taskWindows: über Chocolatey
choco install go-taskWindows: über den Windows Package Manager (winget)
winget install Task.TaskmacOS: über Homebrew (empfohlen)
brew install go-taskLinux: über Apt (Ubuntu/Debian/Mint)
curl -1sLf '@url:`https://dl.cloudsmith.io/public/task/task/setup.deb.sh`' | sudo -E bash
sudo apt install tasksiehe auch Dokumentation
Installation prüfen
task --versionZeigt die installierte Version an (aktuell 3.x).
Hinweis: Task funktioniert unabhängig von deiner Shell. Ob du Git Bash, PowerShell oder
cmdnutzt – dastask-Binary ist dasselbe. Die Wahl der Shell beeinflusst nur, wie die Befehle innerhalb der Tasks ausgeführt werden (siehe Abschnitt 6.5).
6. Einrichtung mit Beispielen
Dieser Abschnitt baut schrittweise eine realistische Taskfile.yml auf. Die Beispiele nutzen Node.js/npm für Build- und Tooling-Aufgaben sowie Docker für Container-Workflows – beides lässt sich aber 1:1 auf beliebige andere Sprachen und Tools übertragen.
6.1 Der minimale Einstieg
Erstelle im Projektstamm eine Datei namens Taskfile.yml:
version: '3'
tasks:
hello:
cmds:
- echo "Hallo Task!"Ausführen:
task helloTask sucht im aktuellen Verzeichnis (und darüber) nach Taskfile.yml und führt den Task hello aus. Ohne Argument führt task den Task namens default aus – im großen Beispiel in 6.8 macht dieser per deps: [build] nichts anderes, als den Build anzustoßen.
6.2 Mehrere Befehle pro Task
version: '3'
tasks:
build:
desc: Baut die Anwendung
cmds:
- echo "Installiere Abhängigkeiten ..."
- npm ci
- echo "Baue das Projekt ..."
- npm run build
- echo "Fertig."desc liefert eine Beschreibung, die task --list anzeigt – nützlich für die Übersicht im Team.
6.3 Dependencies
Ein Task kann andere Tasks voraussetzen. Sie laufen zuerst, in der angegebenen Reihenfolge:
version: '3'
tasks:
build:
deps: [generate, format]
cmds:
- npm run build
generate:
desc: Code generieren
cmds:
- npm run generate
format:
desc: Code formatieren
cmds:
- npx prettier --write .Ein task build führt nun automatisch zuerst generate, dann format, dann den eigentlichen Build aus.
6.4 Variablen und Umgebungsvariablen
version: '3'
vars:
IMAGE: "myapp"
TAG: "1.2.3"
tasks:
build:
desc: "Build frontend application for production"
env:
NODE_ENV: production
cmds:
- npm ci
- npm run build
docker:build:
desc: "Build Docker image using defined variables"
deps: [build]
cmds:
- docker build -t {{.IMAGE}}:{{.TAG}} .varsdefiniert Variablen (global oder pro Task), die per Go-Template-Syntax{{.NAME}}referenziert werden.envsetzt Umgebungsvariablen, die an die aufgerufenen Programme weitergegeben werden.
6.5 Windows & Git Bash: Shell-Auswahl
Das ist der Punkt, den ich mir beim zweiten Anlauf gewünscht hätte. Task führt Befehle standardmäßig in einer plattformspezifischen Shell aus. Mit dem sh:- bzw. cmd:-Präfix oder dem platforms:-Feld steuerst du das Verhalten explizit.
Ein Task, der unter Git Bash (sh) laufen soll:
version: '3'
tasks:
clean:
cmds:
- sh: rm -rf dist/
- sh: find . -name '*.tmp' -deleteEin Task, der plattformabhängig unterschiedliche Befehle nutzt:
version: '3'
tasks:
show-date:
cmds:
- cmd: echo %DATE% %TIME%
platforms: [windows]
- sh: date
platforms: [linux, darwin]So definierst du einen Task, der unter Windows cmd-Befehle nutzt und unter Unix sh. Genau das löst das Ausgangsproblem: Statt zweier getrennter Makefiles pflegst du eine Datei mit klar deklarierten Plattform-Zweigen.
Tipp für Git-Bash-Nutzer:innen: Wenn du Task konsequent aus der Git Bash heraus aufrufst und deine Befehle Unix-Syntax (
rm,cp,find) verwenden sollen, setze dassh:-Präfix – so bist du unabhängig davon, welche Shell Task standardmäßig wählt.
6.6 Caching mit sources und generates
Ein Kern-Feature von Make ist „nur neu bauen, wenn nötig". Task kann das ebenfalls, aber moderner:
version: '3'
tasks:
build:
sources:
- src/**/*.ts
generates:
- dist/index.js
cmds:
- npm run buildsources: die Eingabedateien, deren Änderung einen Rebuild auslöst.generates: die erzeugten Ausgabedateien.
Läuft task build erneut und hat sich keine der sources-Dateien verändert (Zeitstempel- oder inhaltsbasiert), überspringt Task die Ausführung. Anders als bei Make ist dieses Verhalten opt-in: Ohne sources/generates wird ein Task immer ausgeführt. Kein PHONY-Gerangel mehr.
6.7 --watch für Hot-Reload
version: '3'
tasks:
dev:
desc: Dev-Server mit Auto-Reload
cmds:
- npm run dev
sources:
- src/**/*.ts
watch: truetask devTask beobachtet die in sources angegebenen Dateien und startet den Task bei jeder Änderung neu – ideal für lokale Entwicklung.
6.8 Ein vollständiges, gemischtes Beispiel
Hier alles zusammen – Node-Build, Tooling-Pipeline, Dev-Workflow und Docker in einer realistischen Datei:
version: '3'
vars:
IMAGE: myapp
TAG: latest
tasks:
default:
desc: Standard-Task (Alias auf build)
deps: [build]
install:
desc: Abhängigkeiten installieren
cmds:
- npm ci
sources:
- package.json
- package-lock.json
generates:
- node_modules/.package-lock.json
build:
desc: Baut das Projekt
deps: [install]
sources:
- src/**/*.ts
- tsconfig.json
generates:
- dist/index.js
cmds:
- npm run build
format:
desc: Code formatieren
cmds:
- npx prettier --write "src/**/*.ts"
check:
desc: format + lint + test in einem Rutsch
deps: [format, lint, test]
lint:
desc: Linter ausführen
deps: [install]
cmds:
- npm run lint
test:
desc: Tests ausführen
deps: [install]
cmds:
- npm test
clean:
desc: Artefakte entfernen
cmds:
- rm -rf dist node_modules
dev:
desc: Dev-Server starten
deps: [install]
cmds:
- npm run dev
docker-build:
desc: Docker-Image bauen
deps: [build]
cmds:
- docker build -t {{.IMAGE}}:{{.TAG}} .
deploy:
desc: Docker-Image bauen, pushen und auf Server bringen
deps: [docker-build]
cmds:
- docker push {{.IMAGE}}:{{.TAG}}
- ssh deploy@your-server.com "docker pull {{.IMAGE}}:{{.TAG}} && docker compose up -d"Übersicht der wichtigsten Aufrufe:
task # führt den default-Task aus (= build)
task check # format + lint + test in einem Rutsch
task dev # Dev-Server mit Auto-Reload
task --list # alle Tasks mit Beschreibungen anzeigen6.9 Ein reales Beispiel aus der Praxis
Zum Abschluss ein Auszug aus einem realen Projekt: Docker-Images für PHP in mehreren Versionen, aufgeteilt in Apache- und CLI-Variante. Interessant ist vor allem die Variablenbehandlung: Jeder Build akzeptiert die PHP-Version als CLI-Argument (task apache:build -- 8.4), fällt sonst auf den projektweiten Default zurück und leitet daraus den passenden Dockerfile-Namen ab.
version: '3'
vars:
DEFAULT_PHP_VERSION: '8.4'
PHP_APACHE_IMAGE_NAME: 'php-apache'
PHP_APACHE_CONTEXT: 'php-apache'
PHP_APACHE_DOCKERFILE: 'php-apache/Dockerfile'
PHP_CLI_IMAGE_NAME: 'php-cli'
PHP_CLI_CONTEXT: 'php-cli'
PHP_CLI_DOCKERFILE: 'php-cli/Dockerfile'
tasks:
default:
desc: 'Show all available tasks'
cmds:
- task --list
apache:build:
desc: 'Build the PHP Apache Docker image. Usage: task apache:build -- 8.4'
vars:
PHP_VERSION: '{{.CLI_ARGS | default .DEFAULT_PHP_VERSION}}'
PHP_MAJOR_VERSION: '{{index (splitList "." .PHP_VERSION) 0}}'
DOCKERFILE: '{{.PHP_APACHE_DOCKERFILE}}.php{{.PHP_MAJOR_VERSION}}'
cmds:
- >
docker build --build-arg PHP_VERSION={{.PHP_VERSION}} -f {{.DOCKERFILE}}
--no-cache -t {{.PHP_APACHE_IMAGE_NAME}}:{{.PHP_VERSION}} {{.PHP_APACHE_CONTEXT}}
cli:build:
desc: 'Build the PHP CLI Docker image. Usage: task cli:build -- 8.4'
vars:
PHP_VERSION: '{{.CLI_ARGS | default .DEFAULT_PHP_VERSION}}'
PHP_MAJOR_VERSION: '{{index (splitList "." .PHP_VERSION) 0}}'
DOCKERFILE: '{{.PHP_CLI_DOCKERFILE}}.php{{.PHP_MAJOR_VERSION}}'
cmds:
- >
docker build --build-arg PHP_VERSION={{.PHP_VERSION}} -f {{.DOCKERFILE}}
--no-cache -t {{.PHP_CLI_IMAGE_NAME}}:{{.PHP_VERSION}} {{.PHP_CLI_CONTEXT}}
cli:bash:
desc: 'Open a Bash shell in the PHP CLI container. Usage: task cli:bash -- 8.4'
vars:
PHP_VERSION: '{{.CLI_ARGS | default .DEFAULT_PHP_VERSION}}'
cmds:
- docker run --rm -it {{.PHP_CLI_IMAGE_NAME}}:{{.PHP_VERSION}} bash
build:
desc: 'Build both PHP Apache and PHP CLI images'
vars:
PHP_VERSION: '{{.CLI_ARGS | default .DEFAULT_PHP_VERSION}}'
cmds:
- task: apache:build
vars:
PHP_VERSION: '{{.PHP_VERSION}}'
- task: cli:build
vars:
PHP_VERSION: '{{.PHP_VERSION}}'Drei Dinge sind an diesem Beispiel sehenswert: Das Namensschema cli:build gruppiert Tasks sauber nach Thema. Die desc-Zeilen dokumentieren gleich die Verwendung mit ---Argumenten, sodass task --list zur Bedienungsanleitung wird. Der build-Task zeigt, wie man Variablen an Untertasks weiterreicht, statt sie dort erneut aufzulösen. Das Original ist umfangreicher – neben der CLI-Variante gibt es dort denselben Task-Satz für Apache, plus Run- und Bash-Tasks – das Muster ist aber identisch.
7. Fazit
Task ist kein radikaler Bruch mit Make, sondern dessen konsequente Modernisierung. Es bewahrt das, was Make groß gemacht hat (deklarative Tasks, Dependencies, inkrementelle Builds), und ersetzt das, was heute schmerzt (Plattform-Abhängigkeit, kryptische Syntax, Tab-Fallen) durch ein klares, portables YAML-Format.
Ob Build, Code-Generierung, Linting, Dev-Server oder Docker-Workflow: Task deckt die gesamte Bandbreite ab und läuft überall, vom Entwickler-Laptop bis zur CI. Wer bisher zwei Skriptsysteme für Unix und Windows gepflegt hat, kann eines davon löschen.
Weiterführende Links:
Quellen:
- Task-Dokumentation: Installation, Getting Started, Guide, API-Referenz, Templating-Referenz
- Task-Website (taskfile.dev): Angaben zu eingesetzten Projekten und Einsatzgebieten (Stand: August 2026)
- go-task/task#2090: Verhalten von
CLI_ARGSin Taskfile-Variablen
Hinweis zur Erstellung: Textentwurf und Recherche entstanden zum Teil mit KI-Unterstützung, Redaktion und Fachinhalte von mir; das Beitragsbild ist KI-generiert. Fachliche Angaben wurden gegen die offizielle Task-Dokumentation geprüft.