Skip to content

Instantly share code, notes, and snippets.

@FuhuXia
Created July 24, 2026 20:27
Show Gist options
  • Select an option

  • Save FuhuXia/dd2da69508234a8e6f53b70e1677867f to your computer and use it in GitHub Desktop.

Select an option

Save FuhuXia/dd2da69508234a8e6f53b70e1677867f to your computer and use it in GitHub Desktop.
Cloud Foundry CLI completion script
#!/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