Created
July 24, 2026 20:27
-
-
Save FuhuXia/dd2da69508234a8e6f53b70e1677867f to your computer and use it in GitHub Desktop.
Cloud Foundry CLI completion script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # Extended bash completion for the Cloud Foundry CLI (cf CLI v8+) | |
| # | |
| # Covers: | |
| # - Static subcommand/flag completion via go-flags (GO_FLAGS_COMPLETION=1) | |
| # - Dynamic completion of app / org / space / service / buildpack names | |
| # - Position-aware completion for multi-arg commands (e.g. bind-service <app> <service>) | |
| # - Short TTL caching per category to avoid hammering the API on every <Tab> | |
| # | |
| # Install: | |
| # source this file from ~/.bashrc, e.g.: | |
| # [ -f ~/.cf-completion.bash ] && source ~/.cf-completion.bash | |
| _CF_COMPLETION_CACHE_DIR="${TMPDIR:-/tmp}/.cf-completion-cache" | |
| _CF_COMPLETION_TTL=45 # seconds | |
| mkdir -p "$_CF_COMPLETION_CACHE_DIR" 2>/dev/null | |
| # Generic cached lookup: $1 = cache key, $2 = cf command to run, $3 = awk column to extract | |
| _cf_cached_list() { | |
| local key="$1" cmd="$2" col="${3:-1}" | |
| local cache_file="$_CF_COMPLETION_CACHE_DIR/$key" | |
| local now | |
| now=$(date +%s) | |
| if [[ -f "$cache_file" ]]; then | |
| local mtime | |
| mtime=$(stat -f %m "$cache_file" 2>/dev/null || stat -c %Y "$cache_file" 2>/dev/null) | |
| if (( now - mtime < _CF_COMPLETION_TTL )); then | |
| cat "$cache_file" | |
| return 0 | |
| fi | |
| fi | |
| # Each of these commands prints a header block before the actual table; | |
| # `tail -n +5` skips it for `apps`/`orgs`/`spaces`/`services`/`buildpacks`. | |
| # If cf's output format changes across versions, adjust the skip count. | |
| local result | |
| result=$(eval "$cmd" 2>/dev/null | tail -n +5 | awk -v c="$col" '{print $c}' | grep -v '^$') | |
| echo "$result" > "$cache_file" | |
| echo "$result" | |
| } | |
| _cf_app_names() { _cf_cached_list "apps" "cf apps" 1; } | |
| _cf_org_names() { _cf_cached_list "orgs" "cf orgs" 1; } | |
| _cf_space_names() { _cf_cached_list "spaces" "cf spaces" 1; } | |
| _cf_service_names() { _cf_cached_list "services" "cf services" 1; } | |
| _cf_buildpack_names() { _cf_cached_list "buildpacks" "cf buildpacks" 1; } | |
| # Clear the cache manually if you push/delete/rename something and want | |
| # completion to reflect it immediately, rather than waiting out the TTL: | |
| # rm -rf "${TMPDIR:-/tmp}/.cf-completion-cache" | |
| alias cf-completion-flush='rm -rf "${TMPDIR:-/tmp}/.cf-completion-cache"' | |
| # cf CLI built-in short aliases -> canonical subcommand name (from `cf help`). | |
| # Without this, e.g. `cf ds <Tab>` (alias for delete-service) would be treated | |
| # as an unrecognized subcommand and fall into the app-name default. | |
| # | |
| # Implemented as a case statement (not an associative array) for compatibility | |
| # with bash 3.2, which is what macOS still ships by default. | |
| _cf_resolve_alias() { | |
| case "$1" in | |
| l) echo "login" ;; t) echo "target" ;; | |
| h) echo "help" ;; lo) echo "logout" ;; | |
| a) echo "apps" ;; rt) echo "run-task" ;; | |
| p) echo "push" ;; se) echo "set-env" ;; | |
| st) echo "start" ;; sp) echo "stop" ;; | |
| d) echo "delete" ;; rs) echo "restart" ;; | |
| e) echo "env" ;; rg) echo "restage" ;; | |
| m) echo "marketplace" ;; | |
| cups) echo "create-user-provided-service" ;; | |
| s) echo "services" ;; | |
| uups) echo "update-user-provided-service" ;; | |
| cs) echo "create-service" ;; | |
| csk) echo "create-service-key" ;; | |
| dsk) echo "delete-service-key" ;; | |
| ds) echo "delete-service" ;; | |
| sk) echo "service-keys" ;; | |
| bs) echo "bind-service" ;; | |
| brs) echo "bind-route-service" ;; | |
| us) echo "unbind-service" ;; | |
| urs) echo "unbind-route-service" ;; | |
| r) echo "routes" ;; | |
| *) echo "$1" ;; # not an alias — pass through unchanged | |
| esac | |
| } | |
| _cf-cli() | |
| { | |
| local subcmd="${COMP_WORDS[1]}" | |
| subcmd="$(_cf_resolve_alias "$subcmd")" # resolve alias to canonical name | |
| local cur="${COMP_WORDS[COMP_CWORD]}" | |
| local pos=$COMP_CWORD # index of the word currently being completed | |
| # --- Position-aware, multi-arg commands --- | |
| case "$subcmd" in | |
| bind-service|unbind-service) | |
| # cf bind-service APP_NAME SERVICE_INSTANCE | |
| if [[ $pos -eq 2 ]]; then | |
| COMPREPLY=($(compgen -W "$(_cf_app_names)" -- "$cur")); return 0 | |
| elif [[ $pos -eq 3 ]]; then | |
| COMPREPLY=($(compgen -W "$(_cf_service_names)" -- "$cur")); return 0 | |
| fi | |
| ;; | |
| set-org-role|unset-org-role) | |
| # cf set-org-role USERNAME ORG ROLE | |
| if [[ $pos -eq 3 ]]; then | |
| COMPREPLY=($(compgen -W "$(_cf_org_names)" -- "$cur")); return 0 | |
| fi | |
| ;; | |
| add-network-policy|remove-network-policy) | |
| # cf add-network-policy SOURCE_APP DESTINATION_APP --protocol ... --port ... | |
| if [[ $pos -eq 2 || $pos -eq 3 ]]; then | |
| COMPREPLY=($(compgen -W "$(_cf_app_names)" -- "$cur")); return 0 | |
| fi | |
| ;; | |
| esac | |
| # --- Single dynamic-arg commands (arg is always position 2) --- | |
| # | |
| # Default assumption: an unrecognized subcommand's first positional | |
| # argument is an app name, since most `cf` commands operate on an app. | |
| # Only three kinds of subcommand should NOT get that default: | |
| # 1. Ones that take a different entity (org/space/service/buildpack) — mapped explicitly below. | |
| # 2. Ones that take NO positional name arg at all (list commands, auth, plugins, raw API calls, | |
| # or commands whose arg is a brand-new name you're creating, not an existing one to look up). | |
| # 3. Ones already handled above in the position-aware multi-arg block. | |
| if [[ $pos -eq 2 ]]; then | |
| case "$subcmd" in | |
| org|delete-org|rename-org|org-users) | |
| COMPREPLY=($(compgen -W "$(_cf_org_names)" -- "$cur")); return 0 ;; | |
| space|delete-space|rename-space) | |
| COMPREPLY=($(compgen -W "$(_cf_space_names)" -- "$cur")); return 0 ;; | |
| service|delete-service|rename-service|service-key) | |
| COMPREPLY=($(compgen -W "$(_cf_service_names)" -- "$cur")); return 0 ;; | |
| delete-buildpack|rename-buildpack|update-buildpack) | |
| COMPREPLY=($(compgen -W "$(_cf_buildpack_names)" -- "$cur")); return 0 ;; | |
| # No dynamic name to complete at position 2 — fall through to go-flags fallback. | |
| # List/info commands (no positional arg): | |
| apps|orgs|spaces|services|buildpacks|routes|domains|marketplace|plugins|target|help|version) | |
| ;; | |
| # Auth / raw API / plugin management (arg isn't an existing entity name): | |
| login|logout|api|curl|auth|install-plugin|uninstall-plugin|add-plugin-repo|remove-plugin-repo) | |
| ;; | |
| # Creation commands — the arg is a brand-new name, nothing to look up yet: | |
| create-org|create-space|create-service|create-user|create-buildpack|create-service-broker) | |
| ;; | |
| # Everything else: assume it's an app-scoped command. | |
| *) | |
| COMPREPLY=($(compgen -W "$(_cf_app_names)" -- "$cur")); return 0 ;; | |
| esac | |
| fi | |
| # --- target -o / target -s --- | |
| if [[ "$subcmd" == "target" ]]; then | |
| local prev="${COMP_WORDS[$((pos-1))]}" | |
| if [[ "$prev" == "-o" ]]; then | |
| COMPREPLY=($(compgen -W "$(_cf_org_names)" -- "$cur")); return 0 | |
| elif [[ "$prev" == "-s" ]]; then | |
| COMPREPLY=($(compgen -W "$(_cf_space_names)" -- "$cur")); return 0 | |
| fi | |
| fi | |
| # --- Fallback: static go-flags completion for subcommands/flags --- | |
| local args=("${COMP_WORDS[@]:1:$COMP_CWORD}") | |
| local IFS=$'\n' | |
| COMPREPLY=($(GO_FLAGS_COMPLETION=1 "${COMP_WORDS[0]}" "${args[@]}")) | |
| return 0 | |
| } | |
| complete -F _cf-cli cf |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment