Unfortunately, Bash doesn't easily expose its programmable completion functionality to be consumed by other programs. But if you're ok with a rather ugly looking solution, using the following in my .vimrc has been working very well for me:
Note the hardcoded path source /usr/share/bash-completion/bash_completion
in the shell script - this works on Fedora, but it may be different for other distros. Check your /etc/profile to see where it gets sourced from.
command! -nargs=+ -complete=custom,s:BashCmdComp ExtCmd :!<args>
cnoremap ! <c-r>=getcmdpos() == 1 ? "ExtCmd " : "!"<cr>
func s:BashCmdComp(arglead, vimcmdline, vimcursorpos)
" strip vim command
let l:cmdline = substitute(a:vimcmdline, '^\S\+ \(.*\)', '\1', "g")
let l:cursorpos = 1 + a:vimcursorpos - (strlen(a:vimcmdline) - strlen(l:cmdline))
" remove everything after the cursor so that completion can always act on the last word
let l:cmdline = l:cmdline[:l:cursorpos]
if count(l:cmdline, " ") == 0
return system("compgen -A command " . a:arglead)
end
let l:cmdname = substitute(l:cmdline, '^\(\S\+\) .*$', '\1', "")
let l:scriptlines =<< trim eval END
__print_completions() {{
printf '%s\n' "${{COMPREPLY[@]% }}"
}}
# This is the path on Fedora, it may be different for other distros
source /usr/share/bash-completion/bash_completion
_completion_loader {l:cmdname}
COMP_WORDS=({l:cmdline})
COMP_LINE='{l:cmdline}'
COMP_POINT={l:cursorpos}
COMP_CWORD=$((${{#COMP_WORDS[@]}} - 1))
[ $COMP_CWORD -eq 0 ] && COMP_CWORD=1
$(complete -p {l:cmdname} | sed 's/^.*-F \([^ ][^ ]*\).*/\1/')
__print_completions
END
let l:output = trim(system(join(l:scriptlines, "\n")))
if l:output != ""
return l:output
else
return join(getcompletion(a:arglead, "file"), "\n")
endif
endfunc