Bash-Completion-Skripte für eigene CLI-Tools schreiben
AI generated
$_
#!/
Bash · CLI-Tooling · Developer Experience
Bash-Completion-Skripte für eigene CLI-Tools
Mit complete und compgen Unterbefehle, Optionen und dynamische Werte vervollständigen

Ein selbstgeschriebenes CLI-Tool wirkt erst richtig fertig, wenn die Tab-Taste Unterbefehle, Optionen und Argumente vorschlägt, genau wie bei git oder docker. Das Bash-Builtin complete zusammen mit compgen macht das ohne externe Abhängigkeiten möglich, von einer einfachen Wortliste bis zu dynamisch aus einer Datenbank oder API geladenen Vorschlägen.

16 Min. Lesezeit complete · compgen bash-completion · Subcommands

1. Warum eigene Completion die Akzeptanz eines CLI-Tools erhöht

Ein CLI-Tool ohne Tab-Vervollständigung zwingt Nutzer, sich jeden Unterbefehl und jede Option aus dem Gedächtnis oder aus der --help-Ausgabe zu holen, während Werkzeuge wie git, docker oder kubectl per Tab-Taste sofort zeigen, was als Nächstes möglich ist. Diese kleine Investition in eine Completion-Funktion senkt die Einstiegshürde für neue Nutzer eines eigenen Tools spürbar und reduziert Tippfehler bei langen Subcommand-Namen oder Optionsflags.

Gerade bei internen Tools, die von einem ganzen Team genutzt werden, zahlt sich die einmalige Investition in eine Completion-Datei schnell aus, weil jede eingesparte Nachfrage im Chat oder jeder vermiedene Blick in die Dokumentation Zeit spart. Bash-Completion ist dabei kein exotisches Feature, sondern ein Standardmechanismus, den fast jede Linux-Distribution bereits mitbringt und aktiviert hat.

2. Grundlagen: das complete-Builtin und compgen

Das Builtin complete registriert eine Completion-Funktion für einen bestimmten Befehlsnamen, sodass Bash bei jedem Drücken der Tab-Taste innerhalb dieses Befehls die registrierte Funktion aufruft, statt die Standard-Dateinamen-Vervollständigung zu verwenden. Die Funktion selbst befüllt ein spezielles Array namens COMPREPLY mit den Vorschlägen, die Bash danach anzeigt oder bei eindeutigem Treffer direkt einsetzt.

compgen ist das Werkzeug, das innerhalb der Completion-Funktion eine Liste von Kandidaten anhand des bisher eingegebenen Wortfragments filtert. Der Aufruf compgen -W "liste von woertern" "$cur", getrennt durch zwei Bindestriche als Optionsende-Markierung, gibt aus der übergebenen Wortliste nur die Einträge zurück, die mit dem aktuellen Präfix beginnen, und ist damit der Baustein, aus dem sich fast jede Completion-Funktion zusammensetzt.

3. Einfache Wortlisten-Completion für ein eigenes Tool

Der einfachste Einstieg ist eine statische Liste erlaubter Werte, etwa die Namen aller Unterbefehle eines eigenen Tools namens mstool. Die Completion-Funktion liest das aktuell eingegebene Wort aus COMP_WORDS an der Position COMP_CWORD, übergibt es an compgen -W zusammen mit der Wortliste und speichert das Ergebnis in COMPREPLY.

Diese Grundstruktur, bestehend aus dem Auslesen des aktuellen Wortes, dem Filtern per compgen und dem Zuweisen an COMPREPLY, taucht in praktisch jeder Bash-Completion-Funktion wieder auf, unabhängig davon, wie komplex die eigentliche Vorschlagslogik später wird. Wer diese drei Schritte einmal verinnerlicht hat, kann sie auf beliebig viele eigene Tools anwenden.


#!/usr/bin/env bash
# mstool-completion.bash -- einfache Wortlisten-Completion

_mstool_complete() {
  local cur
  cur="${COMP_WORDS[COMP_CWORD]}"
  COMPREPLY=($(compgen -W "deploy status logs rollback" -- "$cur"))
}

complete -F _mstool_complete mstool

4. Unterbefehle vervollständigen: Subcommand-Dispatch in der Completion-Funktion

Sobald ein Tool eigene Argumente pro Unterbefehl anbietet, etwa mstool deploy staging oder mstool logs backend, muss die Completion-Funktion selbst erkennen, welcher Unterbefehl bereits eingegeben wurde, um die passenden Folgevorschläge zu liefern. Dazu wird COMP_WORDS[1] ausgelesen, das bei einem Aufruf wie mstool deploy <TAB> bereits deploy enthält.

Eine case-Anweisung über diesen Unterbefehl verzweigt dann in die jeweils passende Vorschlagsliste, genau wie das Tool selbst intern zwischen seinen Unterbefehlen unterscheidet. Diese Struktur skaliert gut, weil jeder neue Unterbefehl im eigenen Tool nur einen zusätzlichen case-Zweig in der Completion-Funktion braucht, statt die gesamte Logik neu zu schreiben.


#!/usr/bin/env bash
# mstool-completion.bash -- Subcommand-Dispatch

_mstool_complete() {
  local cur subcommand
  cur="${COMP_WORDS[COMP_CWORD]}"
  subcommand="${COMP_WORDS[1]}"

  if [[ "$COMP_CWORD" -eq 1 ]]; then
    COMPREPLY=($(compgen -W "deploy status logs rollback" -- "$cur"))
    return
  fi

  case "$subcommand" in
    deploy)
      COMPREPLY=($(compgen -W "staging production" -- "$cur"))
      ;;
    logs)
      COMPREPLY=($(compgen -W "backend frontend worker" -- "$cur"))
      ;;
  esac
}

complete -F _mstool_complete mstool

5. Optionen und Flags kontextabhängig vervollständigen

Optionen wie --env oder --verbose lassen sich derselben Case-Verzweigung hinzufügen, wobei ein zusätzlicher Check auf den vorherigen Wortfragment sinnvoll ist, um nach --env gezielt Umgebungsnamen statt beliebiger Wörter vorzuschlagen. Der vorherige Wort-Token steckt in COMP_WORDS[COMP_CWORD-1] und lässt sich mit einer zusätzlichen case-Verzweigung auswerten.

Wichtig ist, das aktuelle Wortfragment immer mit einem führenden Bindestrich zu prüfen, um zu erkennen, ob gerade eine Option oder ein normales Argument vervollständigt wird. Beginnt $cur mit -, liefert die Funktion die Liste der verfügbaren Flags, andernfalls die regulären Positions-Argumente wie Umgebungsnamen oder Service-Namen.


#!/usr/bin/env bash
# mstool-completion.bash -- Optionen kontextabhaengig vorschlagen

_mstool_complete() {
  local cur prev
  cur="${COMP_WORDS[COMP_CWORD]}"
  prev="${COMP_WORDS[COMP_CWORD-1]}"

  if [[ "$prev" == "--env" ]]; then
    COMPREPLY=($(compgen -W "staging production local" -- "$cur"))
    return
  fi

  if [[ "$cur" == -* ]]; then
    COMPREPLY=($(compgen -W "--env --verbose --dry-run --help" -- "$cur"))
    return
  fi

  COMPREPLY=($(compgen -W "deploy status logs rollback" -- "$cur"))
}

complete -F _mstool_complete mstool

6. Dynamische Werte vervollständigen: Dateien, IDs aus einer API

Statische Wortlisten reichen nicht mehr aus, sobald Vorschläge von Laufzeitdaten abhängen, etwa gültige Deployment-IDs aus einer Datenbank oder Servicenamen aus einer laufenden Konfiguration. In diesem Fall ruft die Completion-Funktion das eigentliche Tool oder ein Hilfsskript im Hintergrund auf, um die aktuelle Liste zu holen, statt eine feste Liste im Skript zu pflegen.

Solche Aufrufe müssen unbedingt schnell sein, weil sie bei jedem Tab-Druck blockierend ausgeführt werden und eine langsame Netzwerkabfrage die Nutzung des Tools spürbar verlangsamt. Ein kurzes Timeout und, wenn möglich, ein lokaler Cache der letzten Abfrage verhindern, dass eine hängende API die Completion unbenutzbar macht.


#!/usr/bin/env bash
# mstool-completion.bash -- dynamische Werte per Hilfsbefehl

_mstool_complete() {
  local cur subcommand
  cur="${COMP_WORDS[COMP_CWORD]}"
  subcommand="${COMP_WORDS[1]}"

  if [[ "$subcommand" == "rollback" && "$COMP_CWORD" -eq 2 ]]; then
    local ids
    ids=$(timeout 1 mstool internal-list-deployment-ids 2>/dev/null)
    COMPREPLY=($(compgen -W "$ids" -- "$cur"))
    return
  fi
}

complete -F _mstool_complete mstool

7. Completion-Skript strukturieren und lokal testen

Eine Completion-Datei sollte ausschließlich die _toolname_complete-Funktion und den abschließenden complete-Aufruf enthalten, ohne Seiteneffekte beim bloßen Einlesen der Datei, weil sie potenziell bei jedem neuen Shell-Start automatisch geladen wird. Variablen innerhalb der Funktion gehören konsequent mit local deklariert, damit sie nicht versehentlich die interaktive Shell-Umgebung des Nutzers verschmutzen.

Zum Testen reicht es, die Datei mit source in der aktuellen Shell zu laden und anschließend den Befehlsnamen gefolgt von Tab auszuprobieren, ohne die Datei fest zu installieren. Diese schnelle Iteration direkt in der Shell ist deutlich effizienter als jedes Mal eine neue Shell zu öffnen, um eine Änderung an der Completion-Logik zu testen.


#!/usr/bin/env bash
# Lokal testen, ohne die Datei fest zu installieren
source ./mstool-completion.bash

# Danach im Terminal ausprobieren:
# mstool dep<TAB>       -> deploy
# mstool deploy <TAB>   -> staging production
# mstool --e<TAB>       -> --env

8. Installation: wohin die Completion-Datei gehört

Für dauerhafte Nutzung gehört die Completion-Datei nach /etc/bash_completion.d/ oder, auf moderneren Distributionen, nach /usr/share/bash-completion/completions/, benannt exakt wie der Befehl selbst, also mstool ohne Dateiendung. Beide Verzeichnisse werden von der zentralen bash-completion-Infrastruktur automatisch beim Start einer interaktiven Shell eingelesen, ohne dass jeder Nutzer die Datei manuell in seiner .bashrc einbinden muss.

Fehlt das bash-completion-Paket auf dem System, etwa auf minimalen Server-Images, lässt sich die Datei alternativ direkt aus der .bashrc mit source einbinden. Für die Verteilung über ein Paketmanagement-System, etwa via .deb oder Homebrew-Formula, gehört die Completion-Datei mit ins Installationspaket, damit Tab-Vervollständigung direkt nach der Installation funktioniert.


#!/usr/bin/env bash
set -euo pipefail

readonly DEST="/usr/share/bash-completion/completions/mstool"
sudo install -m 644 mstool-completion.bash "$DEST"

echo "Installiert nach $DEST. Neue Shell oeffnen oder 'source $DEST' ausfuehren."

9. Zsh-Kompatibilität und Grenzen der Bash-Completion

Eine mit complete und compgen geschriebene Completion-Funktion läuft ausschließlich in Bash. Zsh-Nutzer, die bashcompinit aktiviert haben, können Bash-Completion-Skripte zwar meist unverändert laden, aber für ein natives Zsh-Erlebnis mit Beschreibungstexten neben jedem Vorschlag ist ein eigenes Zsh-Completion-Skript im _toolname-Format nötig, das eine komplett andere Syntax verwendet.

Für ein internes Tool, das ausschließlich in einer Bash-geprägten Umgebung wie einem Docker-Container oder einer Standard-Server-Distribution läuft, reicht die Bash-Completion allein meist aus. Für ein öffentlich verteiltes CLI-Tool lohnt sich dagegen, von Anfang an sowohl eine Bash- als auch eine Zsh-Completion-Datei zu pflegen, weil ein spürbarer Teil der macOS-Nutzer standardmäßig Zsh verwendet.

Ansatz Aufwand Dynamische Werte Installationsort
Statische Wortliste mit compgen -W Gering Nein /usr/share/bash-completion/completions/
Subcommand-Dispatch mit case Mittel Nein /usr/share/bash-completion/completions/
Dynamische Werte per Hilfsbefehl Höher, Performance beachten Ja Wie oben, mit Timeout absichern
Manuelles source in .bashrc Gering Je nach Funktion Persönliche .bashrc, nicht systemweit
Zsh-natives _toolname-Skript Hoch, eigene Syntax Ja, mit Beschreibungstexten Zsh fpath-Verzeichnis

Mironsoft

Shell-Automatisierung, DevOps-Tooling und Deployment-Infrastruktur

Shell-Skripte, die in der Produktion zuverlässig laufen?

Wir analysieren bestehende Bash-Skripte, erkennen fragile Muster und ersetzen sie durch robuste Bash-Patterns: mit vollständiger Fehlerbehandlung, Logging und sicherer Parallelisierung für euren Deployment-Stack.

Code-Review

ShellCheck-Analyse und manuelle Prüfung auf kritische Bash-Pattern-Verstöße.

Refactoring

Fehlerbehandlung, Logging und sichere Dateioperationen nachrüsten.

CI-Integration

ShellCheck und BATS in Pipelines integrieren und Regressionstests aufbauen.

10. Zusammenfassung

Bash-Completion-Skripte für eigene CLI-Tools: Das Wichtigste auf einen Blick

Grundprinzip

complete registriert eine Funktion pro Befehl, die das COMPREPLY-Array befüllt. compgen -W filtert Kandidaten nach dem aktuellen Wortpräfix.

Subcommands

COMP_WORDS[1] liefert den bereits eingegebenen Unterbefehl, eine case-Anweisung liefert passende Folgevorschläge.

Dynamische Werte

Bei Werten aus API oder Datenbank das Hilfsskript mit kurzem Timeout aufrufen, um die Completion nicht zu blockieren.

Installation

Datei nach /usr/share/bash-completion/completions/toolname legen, benannt exakt wie der Befehl, ohne Dateiendung.

11. FAQ: Bash-Completion-Skripte für eigene CLI-Tools: Das Wichtigste auf einen Blick

1Was macht das complete-Builtin genau?
complete registriert eine Funktion für einen Befehlsnamen, die Bash bei jedem Tab-Druck innerhalb dieses Befehls aufruft, statt die Standard-Dateinamen-Vervollständigung zu verwenden.
2Wofür ist compgen zuständig?
compgen filtert eine Liste von Kandidaten anhand des aktuell eingegebenen Wortfragments und gibt nur die passenden Treffer zurück, die dann in COMPREPLY landen.
3Wie erkenne ich in der Completion-Funktion, welcher Unterbefehl schon eingegeben wurde?
Über COMP_WORDS[1], das bei einem Aufruf wie tool deploy bereits den Wert deploy enthält, ausgewertet in einer case-Anweisung.
4Wie vervollständige ich Optionen nur, wenn das aktuelle Wort mit einem Bindestrich beginnt?
Mit einem Check ob $cur mit -* beginnt, und in diesem Fall die Liste der Flags statt der regulären Argumente an compgen übergeben.
5Wie baue ich dynamische Vorschläge aus einer API oder Datenbank ein?
Die Completion-Funktion ruft ein Hilfsskript oder das Tool selbst im Hintergrund auf, um die aktuelle Liste zu holen, abgesichert mit einem kurzen timeout, damit eine langsame Antwort die Completion nicht blockiert.
6Wie teste ich eine Completion-Funktion, ohne sie zu installieren?
Mit source ./meine-completion.bash in der aktuellen Shell laden und danach direkt den Befehl mit Tab ausprobieren, ohne die Datei fest zu installieren.
7Wo installiere ich eine Bash-Completion-Datei dauerhaft?
Nach /usr/share/bash-completion/completions/ oder /etc/bash_completion.d/, benannt exakt wie der Befehl, ohne Dateiendung, damit die bash-completion-Infrastruktur sie automatisch lädt.
8Was, wenn das bash-completion-Paket auf dem System fehlt?
Dann lässt sich die Completion-Datei alternativ direkt mit source in der persönlichen .bashrc einbinden, allerdings nur für den einzelnen Nutzer statt systemweit.
9Funktioniert eine Bash-Completion-Datei auch in Zsh?
Mit aktiviertem bashcompinit meist ja, aber ohne die nativen Zsh-Features wie Beschreibungstexte neben jedem Vorschlag. Für ein natives Erlebnis ist ein eigenes Zsh-Completion-Skript nötig.
10Warum sollten Variablen in der Completion-Funktion mit local deklariert werden?
Damit sie nicht versehentlich die interaktive Shell-Umgebung des Nutzers verschmutzen, da die Funktion bei jedem Tab-Druck im Kontext der aktuellen Shell ausgeführt wird.