I have requests from one of my consulting clients to add support for auto-completing target names when typing commands like cmake --build . --target <TAB>. I’ve often wanted something similar myself. I’ve been thinking about this for a while, and I’m wondering if we can use the most recent replies of the CMake file API to provide the details for this to work. Not sure yet quite how to hook it all up, given that the only things you can rely on being installed are CMake and bash, so no JSON-processing tools, but maybe CMake’s own JSON processing would be good enough. In theory, the bash completion could run CMake scripts to provide the results, so I think there’s potentially a viable path, although performance may be something to keep a close eye on.
Before I go spending on any time on this, I wanted to ask if anyone has any thoughts or objections to such an approach? It would have to rely on something having made a file API query in an earlier CMake run to work, but that seems like a reasonable minimum requirement for such functionality to work.
1 Like
brad.king
(Brad King)
January 18, 2024, 10:56pm
2
Perhaps a cmake -E bash-completion-helper tool could be added to do the heavy lifting.
CMake Issue 19168 discusses an idea to let users opt-in to having all their CMake build trees get file-api replies automatically. That could be useful in conjunction with this.
3 Likes
kisielk
(Kamil Kisiel)
July 16, 2024, 7:15pm
3
I would love to have this for --preset as well.
Auto-completion for preset names should already work. I use it often in my daily work. You might need to source certain files to get it enabled though (I had to add some things to the ~/.bashrc due to some quirks with how bash completions work or how they load things, never really figured out precisely why they didn’t work out-of-the-box).
kisielk
(Kamil Kisiel)
July 16, 2024, 11:43pm
5
Huh, and so it does. It wasn’t working in a particular terminal I was trying it on at the time, but that was within an IDE, must be something with the way the bash environment was being loaded there
Auto-completion also works for Ninja generator.
ninja -C build <tab>
Display all 311 possibilities? (y or n)
Lieven
(de Cock)
February 9, 2026, 1:17pm
7
Hello, is there any progress on the idea, or implementation.
Indeed both with make and ninja one can do this, for me this is typically the only reason I descend into the binary dir. If cmake –build –target … would provide this, I think 99% of the time I would no longer go into the binary dir.
I’m not aware of anyone working on this feature. Bash completion gets very little attention, and few contributors dig into it. The implementation is not easy to work with, I found it quite difficult to work out how to debug and fix things back when I worked on some things a fair while back. I don’t anticipate having any time to work on it further in the foreseeable future, so unless someone else steps up to take a crack at it, I don’t expect any progress.
ClausKlein
(Claus Klein)
February 14, 2026, 8:54am
9
I am working either with cmake workflow presets or I change to the build directory and work with ninja, sometime ctest or cpack on developer console.
That works fine on every build system.
craig.scott
(Craig Scott)
February 14, 2026, 10:18am
10
The original request is about completing target names after --target when doing cmake --build. That auto-completion remains unimplemented.
Lieven
(de Cock)
March 3, 2026, 3:23pm
11
just for reference:
cmake --build --target help
lists all the targets, so this could be used as input to the tab completion mechanism (I have no clue how such a thing works, and maybe this is of no use after all0 ?
on my iMac with brew installed CMake, I have this symbolic link /usr/local/etc/bash_completion.d/cmake
@craig.scott @brad.king this files belongs to CMake?
bash-5.3$ brew list --verbose cmake | grep bash_completion.d/cmake
/usr/local/Cellar/cmake/4.2.3/etc/bash_completion.d/cmake
bash-5.3$
# bash completion for cmake(1) -*- shell-script -*-
_cmake()
{
local is_old_completion=false
local is_init_completion=false
local cur prev words cword split was_split
if type -t _comp_initialize >/dev/null; then
_comp_initialize -s || return
elif type -t _init_completion >/dev/null; then
_init_completion -s || return
is_init_completion=true
else
# manual initialization for older bash completion versions
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
is_old_completion=true
split=false
fi
# Workaround for options like -DCMAKE_BUILD_TYPE=Release
local prefix=
if [[ $cur == -D* ]]; then
prev=-D
prefix=-D
cur="${cur#-D}"
elif [[ $cur == -U* ]]; then
prev=-U
prefix=-U
cur="${cur#-U}"
fi
case "$prev" in
-D)
if [[ $cur == *=* ]]; then
# complete values for variables
local var type value
var="${cur%%[:=]*}"
value="${cur#*=}"
if [[ $cur == CMAKE_BUILD_TYPE* ]]; then # most widely used case
COMPREPLY=( $( compgen -W 'Debug Release RelWithDebInfo
MinSizeRel' -- "$value" ) )
return
fi
if [[ $cur == *:* ]]; then
type="${cur#*:}"
type="${type%%=*}"
else # get type from cache if it's not set explicitly
type=$( cmake -LA -N 2>/dev/null | grep "$var:" \
2>/dev/null )
type="${type#*:}"
type="${type%%=*}"
fi
case "$type" in
FILEPATH)
cur="$value"
_filedir
return
;;
PATH)
cur="$value"
_filedir -d
return
;;
BOOL)
COMPREPLY=( $( compgen -W 'ON OFF TRUE FALSE' -- \
"$value" ) )
return
;;
STRING|INTERNAL)
# no completion available
return
;;
esac
elif [[ $cur == *:* ]]; then
# complete types
local type="${cur#*:}"
COMPREPLY=( $( compgen -W 'FILEPATH PATH STRING BOOL INTERNAL'\
-S = -- "$type" ) )
compopt -o nospace
else
# complete variable names
COMPREPLY=( $( compgen -W '$( cmake -LA -N 2>/dev/null |
tail -n +2 | cut -f1 -d: )' -P "$prefix" -- "$cur" ) )
compopt -o nospace
fi
return
;;
-U)
COMPREPLY=( $( compgen -W '$( cmake -LA -N | tail -n +2 |
cut -f1 -d: )' -P "$prefix" -- "$cur" ) )
return
;;
esac
if $is_old_completion; then
_split_longopt && split=true
fi
case "$prev" in
-C|-P|--graphviz|--system-information)
_filedir
return
;;
--build)
# Seed the reply with non-directory arguments that we know are
# allowed to follow --build. _filedir will then prepend any valid
# directory matches to these.
COMPREPLY=( $( compgen -W "--preset --list-presets" -- "$cur" ) )
_filedir -d
return
;;
--install|--open)
_filedir -d
return
;;
-E)
COMPREPLY=( $( compgen -W "$( cmake -E help |& sed -n \
'/^ [^ ]/{s|^ \([^ ]\{1,\}\) .*$|\1|;p}' 2>/dev/null )" \
-- "$cur" ) )
return
;;
-G)
local IFS=$'\n'
local quoted
printf -v quoted %q "$cur"
COMPREPLY=( $( compgen -W '$( cmake --help 2>/dev/null | sed -n \
-e "1,/^Generators/d" \
-e "/^ *[^ =]/{s|^ *\([^=]*[^ =]\).*$|\1|;s| |\\\\ |g;p}" \
2>/dev/null )' -- "$quoted" ) )
return
;;
--loglevel)
COMPREPLY=( $(compgen -W 'error warning notice status verbose debug trace' -- $cur ) )
;;
--help-command)
COMPREPLY=( $( compgen -W '$( cmake --help-command-list 2>/dev/null|
grep -v "^cmake version " )' -- "$cur" ) )
return
;;
--help-manual)
COMPREPLY=( $( compgen -W '$( cmake --help-manual-list 2>/dev/null|
grep -v "^cmake version " | sed -e "s/([0-9])$//" )' -- "$cur" ) )
return
;;
--help-module)
COMPREPLY=( $( compgen -W '$( cmake --help-module-list 2>/dev/null|
grep -v "^cmake version " )' -- "$cur" ) )
return
;;
--help-policy)
COMPREPLY=( $( compgen -W '$( cmake --help-policy-list 2>/dev/null |
grep -v "^cmake version " )' -- "$cur" ) )
return
;;
--help-property)
COMPREPLY=( $( compgen -W '$( cmake --help-property-list \
2>/dev/null | grep -v "^cmake version " )' -- "$cur" ) )
return
;;
--help-variable)
COMPREPLY=( $( compgen -W '$( cmake --help-variable-list \
2>/dev/null | grep -v "^cmake version " )' -- "$cur" ) )
return
;;
--list-presets)
local IFS=$'\n'
local quoted
printf -v quoted %q "$cur"
if [[ ! "${IFS}${COMP_WORDS[*]}${IFS}" =~ "${IFS}--build${IFS}" ]]; then
COMPREPLY=(
$( compgen -W "configure${IFS}build${IFS}package${IFS}test${IFS}workflow${IFS}all" -- "$quoted" )
)
fi
return
;;
--preset)
local IFS=$'\n'
local quoted
printf -v quoted %q "$cur"
local preset_type
if [[ "${IFS}${COMP_WORDS[*]}${IFS}" =~ "${IFS}--workflow${IFS}" ]]; then
preset_type="workflow"
elif [[ "${IFS}${COMP_WORDS[*]}${IFS}" =~ "${IFS}--build${IFS}" ]]; then
preset_type="build"
else
preset_type="configure"
fi
local presets=$( cmake --list-presets="$preset_type" 2>/dev/null |
grep -o "^ \".*\"" | sed \
-e "s/^ //g" \
-e "s/\"//g" \
-e 's/ /\\\\ /g' )
COMPREPLY=( $( compgen -W "$presets" -- "$quoted" ) )
return
;;
--workflow)
local quoted
printf -v quoted %q "$cur"
# Options allowed right after `--workflow`
local workflow_options='--preset --list-presets --fresh'
if [[ "$cur" == -* ]]; then
COMPREPLY=( $( compgen -W "$workflow_options" -- "$quoted" ) )
else
local presets=$( cmake --list-presets=workflow 2>/dev/null |
grep -o "^ \".*\"" | sed \
-e "s/^ //g" \
-e "s/\"//g" \
-e 's/ /\\\\ /g' )
COMPREPLY=( $( compgen -W "$presets $workflow_options" -- "$quoted" ) )
fi
return
;;
esac
if ($is_old_completion || $is_init_completion); then
$split && return
else
[[ $was_split ]] && return
fi
if [[ "$cur" == -* ]]; then
# FIXME(#26100): `cmake --help` is missing some options and does not
# have any mode-specific options like `cmake --build`'s `--config`.
COMPREPLY=( $(compgen -W '$( _parse_help "$1" --help )' -- ${cur}) )
[[ $COMPREPLY == *= ]] && compopt -o nospace
[[ $COMPREPLY ]] && return
fi
_filedir
} &&
complete -F _cmake cmake
# ex: ts=4 sw=4 et filetype=sh
if you insert this code after –-build) case block:
--target)
local quoted
printf -v quoted %q "$cur"
# Options allowed right after `--target`
local target_options='--verbose --clean-first --config --'
local build_dir="build"
for ((i=0; i < ${#COMP_WORDS[@]}; i++)); do
if [[ "${COMP_WORDS[i]}" == "--build" ]]; then
build_dir="${COMP_WORDS[i+1]}"
break
fi
done
if [[ "$cur" == -* ]]; then
COMPREPLY=( $( compgen -W "$target_options" -- "$quoted" ) )
else
local targets=$( cmake --build "$build_dir" --target help 2>/dev/null \
| sed -e 's/: .*$//g' )
COMPREPLY=( $( compgen -W "$targets $target_options" -- "$quoted" ) )
fi
return
;;
it works like with ninja -C build/ and I tested it only with ninja generator!
Not perfect, but use it as HINT: you have to write –-target <tab> yourself
That works out of the CMake box with bash completion at least on a UNIX like system
bash-5.3$ cmake --build --
--list-presets --preset
bash-5.3$ cmake --build --list-presets
CMake Error: Could not read presets from /Users/clausklein/Downloads/cmake:
File not found: /Users/clausklein/Downloads/cmake/CMakePresets.json
bash-5.3$ ll *.json
ls: cannot access '*.json': No such file or directory
bash-5.3$ cmake --build build/ --t
--toolchain --trace --trace-expand --trace-format --trace-redirect --trace-source
bash-5.3$ cmake --build build/ --target
Display all 524 possibilities? (y or n)
Claus Klein:
on my iMac with brew installed CMake, I have this symbolic link /usr/local/etc/bash_completion.d/cmake
@craig.scott @brad.king this files belongs to CMake?
I don’t believe this is from official upstream CMake. This is most likely something done by homebrew.
brad.king
(Brad King)
March 3, 2026, 9:38pm
16
We do have Auxiliary/bash-completion in the source tree.
@brad.king Do you want a PR?
and who can explain, why –target is not expanded?
bash-5.3$ cmake --build build/ --t
--toolchain --trace --trace-expand --trace-format --trace-redirect --trace-source
bash-5.3$ cmake --build build/ --target
Display all 524 possibilities? (y or n)
I should have been clearer. I meant the symlink in /usr/local/etc/bash_completion.d is likely to be from homebrew. The file it points to probably is the one from official CMake.
This change works for both ninja and make:
# bash completion for cmake(1) -*- shell-script -*-
_cmake()
{
local cur prev words cword split=false
if type -t _init_completion >/dev/null; then
_init_completion -n = || return
else
# manual initialization for older bash completion versions
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
fi
# Workaround for options like -DCMAKE_BUILD_TYPE=Release
local prefix=
if [[ $cur == -D* ]]; then
prev=-D
prefix=-D
cur="${cur#-D}"
elif [[ $cur == -U* ]]; then
prev=-U
prefix=-U
cur="${cur#-U}"
fi
# Check if we are in a multi-target list after --target or -t
local in_target_list=false
local i
for (( i=COMP_CWORD-1; i > 0; i-- )); do
if [[ "${COMP_WORDS[i]}" == --target || "${COMP_WORDS[i]}" == -t ]]; then
in_target_list=true
break
elif [[ "${COMP_WORDS[i]}" == -* ]]; then
# Another option started, so we are no longer in the target list
break
fi
done
if $in_target_list; then
local build_dir
for (( i=1; i < COMP_CWORD; i++ )); do
if [[ "${COMP_WORDS[i]}" == --build ]]; then
build_dir="${COMP_WORDS[i+1]}"
break
fi
done
if [[ -n "$build_dir" && -d "$build_dir" ]]; then
local targets=$( cmake --build "$build_dir" --target help 2>/dev/null | \
sed -n -e 's/^... \([^ ]*\).*$/\1/p' \
-e '/^\[/d' -e 's/^\([^ :]*\):.*$/\1/p' | \
grep -v '^/' | sort -u )
COMPREPLY=( $( compgen -W "$targets" -- "$cur" ) )
return
fi
fi
case "$prev" in
-D)
if [[ $cur == *=* ]]; then
# complete values for variables
local var type value
var="${cur%%[:=]*}"
value="${cur#*=}"
if [[ $cur == CMAKE_BUILD_TYPE* ]]; then # most widely used case
COMPREPLY=( $( compgen -W 'Debug Release RelWithDebInfo
MinSizeRel' -- "$value" ) )
return
fi
if [[ $cur == *:* ]]; then
type="${cur#*:}"
type="${type%%=*}"
else # get type from cache if it's not set explicitly
type=$( cmake -LA -N 2>/dev/null | grep "$var:" \
2>/dev/null )
type="${type#*:}"
type="${type%%=*}"
fi
case "$type" in
FILEPATH)
cur="$value"
_filedir
return
;;
PATH)
cur="$value"
_filedir -d
return
;;
BOOL)
COMPREPLY=( $( compgen -W 'ON OFF TRUE FALSE' -- \
"$value" ) )
return
;;
STRING|INTERNAL)
# no completion available
return
;;
esac
elif [[ $cur == *:* ]]; then
# complete types
local type="${cur#*:}"
COMPREPLY=( $( compgen -W 'FILEPATH PATH STRING BOOL INTERNAL'\
-S = -- "$type" ) )
compopt -o nospace
else
# complete variable names
COMPREPLY=( $( compgen -W '$( cmake -LA -N 2>/dev/null |
tail -n +2 | cut -f1 -d: )' -P "$prefix" -- "$cur" ) )
compopt -o nospace
fi
return
;;
-U)
COMPREPLY=( $( compgen -W '$( cmake -LA -N | tail -n +2 |
cut -f1 -d: )' -P "$prefix" -- "$cur" ) )
return
;;
esac
_split_longopt && split=true
case "$prev" in
-C|-P|--graphviz|--system-information)
_filedir
return
;;
--build)
# Seed the reply with non-directory arguments that we know are
# allowed to follow --build. _filedir will then prepend any valid
# directory matches to these.
COMPREPLY=( $( compgen -W "--preset --list-presets --target --config --clean-first --parallel --verbose" -- "$cur" ) )
_filedir -d
return
;;
--install|--open)
_filedir -d
return
;;
-E)
COMPREPLY=( $( compgen -W "$( cmake -E help |& sed -n \
'/^ [^ ]/{s|^ \([^ ]\{1,\}\) .*$|\1|;p}' 2>/dev/null )" \
-- "$cur" ) )
return
;;
-G)
local IFS=$'\n'
local quoted
printf -v quoted %q "$cur"
COMPREPLY=( $( compgen -W '$( cmake --help 2>/dev/null | sed -n \
-e "1,/^Generators/d" \
-e "/^ *[^ =]/{s|^ *\([^=]*[^ =]\).*$|\1|;s| |\\\\ |g;p}" \
2>/dev/null )' -- "$quoted" ) )
return
;;
--loglevel)
COMPREPLY=( $(compgen -W 'error warning notice status verbose debug trace' -- $cur ) )
;;
--help-command)
COMPREPLY=( $( compgen -W '$( cmake --help-command-list 2>/dev/null|
grep -v "^cmake version " )' -- "$cur" ) )
return
;;
--help-manual)
COMPREPLY=( $( compgen -W '$( cmake --help-manual-list 2>/dev/null|
grep -v "^cmake version " | sed -e "s/([0-9])$//" )' -- "$cur" ) )
return
;;
--help-module)
COMPREPLY=( $( compgen -W '$( cmake --help-module-list 2>/dev/null|
grep -v "^cmake version " )' -- "$cur" ) )
return
;;
--help-policy)
COMPREPLY=( $( compgen -W '$( cmake --help-policy-list 2>/dev/null |
grep -v "^cmake version " )' -- "$cur" ) )
return
;;
--help-property)
COMPREPLY=( $( compgen -W '$( cmake --help-property-list \
2>/dev/null | grep -v "^cmake version " )' -- "$cur" ) )
return
;;
--help-variable)
COMPREPLY=( $( compgen -W '$( cmake --help-variable-list \
2>/dev/null | grep -v "^cmake version " )' -- "$cur" ) )
return
;;
--list-presets)
local IFS=$'\n'
local quoted
printf -v quoted %q "$cur"
if [[ ! "${IFS}${COMP_WORDS[*]}${IFS}" =~ "${IFS}--build${IFS}" ]]; then
COMPREPLY=( $( compgen -W "configure${IFS}build${IFS}test${IFS}all" -- "$quoted" ) )
fi
return
;;
--preset)
local IFS=$'\n'
local quoted
printf -v quoted %q "$cur"
local build_or_configure="configure"
if [[ "${IFS}${COMP_WORDS[*]}${IFS}" =~ "${IFS}--build${IFS}" ]]; then
build_or_configure="build"
fi
local presets=$( cmake --list-presets="$build_or_configure" 2>/dev/null |
grep -o "^ \".*\"" | sed \
-e "s/^ //g" \
-e "s/\"//g" \
-e 's/ /\\\\ /g' )
COMPREPLY=( $( compgen -W "$presets" -- "$quoted" ) )
return
;;
esac
$split && return
if [[ "$cur" == -* ]]; then
COMPREPLY=( $(compgen -W '$( _parse_help "$1" --help )' -- ${cur}) )
[[ $COMPREPLY == *= ]] && compopt -o nospace
[[ $COMPREPLY ]] && return
fi
_filedir
} &&
complete -F _cmake cmake
# ex: ts=4 sw=4 et filetype=sh
ClausKlein
(Claus Klein)
March 3, 2026, 10:04pm
20
that are sysmlinks created from homebrew, no real package is install at /usr/local