7

I'm using SaltStack. I would like to auto-complete the minion name when calling the salt command.

The following line has been added into ~/.bashrc:

complete -o default -o nospace -W "$(sudo ls -1 /var/cache/salt/master/minions)" salt

Then typing salt inTabsalt integration-Tab; I can see it works as expected:

$ salt integration-TabTab
integration-c   integration-u   integration-u2

To use with sudo, I have added complete -cf sudo into ~/.bashrc, but it didn't work:

    sudo salt inTab

    returned nothing.

    I also have tried to install bash_completion and added the following lines to ~/.bash_profile:

    if [ -f $(brew --prefix)/etc/bash_completion ]; then
        . $(brew --prefix)/etc/bash_completion
    fi
    

    but no luck.

    Did I miss something?


    Update

    Oh, the first thing I would like to say is sometimes it works:

    $ sudo salt integration-TabTab
    integration-c   integration-u   integration-u2

    and sometimes it doesn't.

    So first, let's see how much your bash_completion package does.

    How can I check that? Here's my function:

    # a wrapper method for the next one, when the offset is unknown
    _command()
    {
        local offset i
    
        # find actual offset, as position of the first non-option
        offset=1
        for (( i=1; i <= COMP_CWORD; i++ )); do
            if [[ "${COMP_WORDS[i]}" != -* ]]; then
                offset=$i
                break
            fi
        done
        _command_offset $offset
    }
    
    # A meta-command completion function for commands like sudo(8), which need to
    # first complete on a command, then complete according to that command's own
    # completion definition - currently not quite foolproof (e.g. mount and umount
    # don't work properly), but still quite useful.
    #
    _command_offset()
    {
        local cur func cline cspec noglob cmd i char_offset word_offset \
            _COMMAND_FUNC _COMMAND_FUNC_ARGS
    
        word_offset=$1
    
        # rewrite current completion context before invoking
        # actual command completion
    
        # find new first word position, then
        # rewrite COMP_LINE and adjust COMP_POINT
        local first_word=${COMP_WORDS[$word_offset]}
        for (( i=0; i <= ${#COMP_LINE}; i++ )); do
            if [[ "${COMP_LINE:$i:${#first_word}}" == "$first_word" ]]; then
                char_offset=$i
                break
            fi
        done
        COMP_LINE=${COMP_LINE:$char_offset}
        COMP_POINT=$(( COMP_POINT - $char_offset ))
    
        # shift COMP_WORDS elements and adjust COMP_CWORD
        for (( i=0; i <= COMP_CWORD - $word_offset; i++ )); do
            COMP_WORDS[i]=${COMP_WORDS[i+$word_offset]}
        done
        for (( i; i <= COMP_CWORD; i++ )); do
            unset COMP_WORDS[i];
        done
        COMP_CWORD=$(( $COMP_CWORD - $word_offset ))
    
        COMPREPLY=()
        _get_comp_words_by_ref cur
    
        if [[ $COMP_CWORD -eq 0 ]]; then
            _compopt_o_filenames
            COMPREPLY=( $( compgen -c -- "$cur" ) )
        else
            cmd=${COMP_WORDS[0]}
            if complete -p ${cmd##*/} &>/dev/null; then
                cspec=$( complete -p ${cmd##*/} )
                if [ "${cspec#* -F }" != "$cspec" ]; then
                    # complete -F <function>
    
                    # get function name
                    func=${cspec#*-F }
                    func=${func%% *}
    
                    if [[ ${#COMP_WORDS[@]} -ge 2 ]]; then
                        $func $cmd "${COMP_WORDS[${#COMP_WORDS[@]}-1]}" "${COMP_WORDS[${#COMP_WORDS[@]}-2]}"
                    else
                        $func $cmd "${COMP_WORDS[${#COMP_WORDS[@]}-1]}"
                    fi
    
                    # remove any \: generated by a command that doesn't
                    # default to filenames or dirnames (e.g. sudo chown)
                    # FIXME: I'm pretty sure this does not work!
                    if [ "${cspec#*-o }" != "$cspec" ]; then
                        cspec=${cspec#*-o }
                        cspec=${cspec%% *}
                        if [[ "$cspec" != @(dir|file)names ]]; then
                            COMPREPLY=("${COMPREPLY[@]//\\\\:/:}")
                        else
                            _compopt_o_filenames
                        fi
                    fi
                elif [ -n "$cspec" ]; then
                    cspec=${cspec#complete};
                    cspec=${cspec%%${cmd##*/}};
                    COMPREPLY=( $( eval compgen "$cspec" -- "$cur" ) );
                fi
            elif [ ${#COMPREPLY[@]} -eq 0 ]; then
                _filedir
            fi
        fi
    }
    

    if you type sudo mkdirTabTab, does it show a list of directories?

    Yes:

    $ sudo mkdir TabTab
    .FontForge/        .djangopypi2/      .ievms/            .ssh/              .wireshark-etc/
    quanta
    • 1,670
    • 1
    • 14
    • 25
    • 1
      Chaining together command completions usually involves completion functions that invoke a function defined in the completion package called `_command_offset`. So first, let's see how much your bash_completion package does. If you remove the `complete -cf sudo` line from .bashrc, then logout and login again, and just use the support built in to bash_completion, do you see any special handling at all for sudo, e.g., if you type `sudo mkdir `, does it show a list of directories? – Mark Plotnick Jul 11 '14 at 14:38
    • @MarkPlotnick: _if you type sudo `mkdir `, does it show a list of directories?_ --> Yes, see my update in the original question. – quanta Jul 12 '14 at 04:49
    • 1
      If completion chaining is sometimes working and sometimes not, the first thing I'd check is the completion definition for `sudo` when completions are not working. Do this by typing `complete|grep sudo`. Bash on OSX reads its init files in nonintuitive ways (see http://apple.stackexchange.com/a/13019), so my hunch is that sometimes not all your completion definitions are being read in or they are being superseded by unwanted definitions. – Mark Plotnick Jul 14 '14 at 21:29
    • Possibly related: [How does Bash path completion work with sudo?](http://unix.stackexchange.com/q/30827/80216) – G-Man Says 'Reinstate Monica' Jun 27 '15 at 08:41

    3 Answers3

    3

    Check set -o to see if maybe posix mode is enabled. If so, disable with set +o posix

    In "posix" mode, bash's tab-completion is for some reason disabled in "vi-mode". I've not seen any explanation why this behavior is so, nor why it's specific to vi-mode, so I didn't bother explaining.

    PS: This answer is more suited for someone who stumbles upon the question via a search engine... not for your particular setup.

    Update:
    Chet Ramey says:

    in POSIX mode, the vi editing mode should not map tab to complete. This is because POSIX.2 completely specifies the behavior of the vi editing mode, and the standard requires tab to be mapped to self insert by default.

    hildred
    • 5,759
    • 3
    • 30
    • 43
    Otheus
    • 5,945
    • 1
    • 22
    • 53
    1

    Quick answer:

    • Install bash-completion
    • Source bash_completion on Bash startup
    • Add your compspec on Bash startup
    • Don't overwrite sudo compspec with complete -cf sudo

    I suppose that you use MacOSX with brew.

    Try:

    brew update
    brew install bash-completion
    brew info bash-completion
    # bash-completion: stable 1.3
    . $(brew --prefix)/etc/bash_completion
    complete -p sudo
    

    You should see something like this:

    complete -F _root_command sudo
    

    Test:

    function _comp_foo() { COMPREPLY=($(compgen -W 'a b c' -- "$2")); }
    complete -F _comp_foo foo
    

    Type foo SpaceTabTab
    You should see a b c

    Type sudo foo SpaceTabTab
    You should see a b c

    Then remove complete -fc sudo from your initialisation files (~/.bash_profile, ~/.bashrc etc)
    Add the following lines to your initialisation files:

    if [ -f $(brew --prefix)/etc/bash_completion ]; then
      . $(brew --prefix)/etc/bash_completion
    fi
    
    # Your compspec here
    complete -o default -o nospace -W "$(sudo ls -1 /var/cache/salt/master/minions)" salt
    

    Reopen terminal.
    Type complete -p sudo.
    You should see complete -F _root_command sudo.

    Type complete -p salt.
    You should see something like this:

    complete -o default -o nospace -W 'a
    b
    c' salt
    

    bash-completion2.* note:
    You can install bash-completion2: https://github.com/Homebrew/homebrew/issues/19258
    But:

    • 2.* works with Bash 4.*
    • 2.* uses the -D option for compspecs lazy loading. So, complete -p sudo outputs complete: sudo: no completion specification until you type sudoSpaceTab
    Stephen Kitt
    • 411,918
    • 54
    • 1,065
    • 1,164
    Evgeny Vereshchagin
    • 5,286
    • 4
    • 35
    • 43
    -1

    sudo doesn't give you an login shell unless you ask for it with -i. So you'll find that without it you'll need to load the system's completions:

    . /etc/bash_completion
    

    If you run with -i, then $HOME will be ~root, and so anything written to the user's ~/.bashrc won't get read. If you can separate out the completion stuff from the rest of your ~/.bashrc, source that with . and that should work.

    Toby Speight
    • 8,460
    • 3
    • 26
    • 50
    • But this question is about somebody typing a `sudo` command *into his default shell*.  `sudo` hasn't started running yet, so ***its*** behavior is irrelevant; this is all about the behavior of the user's shell. – G-Man Says 'Reinstate Monica' Jun 27 '15 at 07:56