Bash autocompletion of CMake targets

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

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

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).

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)

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.

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.

The original request is about completing target names after --target when doing cmake --build. That auto-completion remains unimplemented.

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)

I don’t believe this is from official upstream CMake. This is most likely something done by homebrew.

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

that are sysmlinks created from homebrew, no real package is install at /usr/local