The issue is specifically that shell builtins work to <tab> expand parameter names, but external commands do not. this is why cd works. echo, read, and any other builtin continue to work like OP expects.
nothing to do with shopts.
It's in the bash_completion library (/etc/bash_completion in my 12.04.4 system). Something in release 1.3 exists which causes this failure situation. somewhere at-or-before 1.99 it was fixed.
The bash_completion lib will automatically source ~/.bash_completion if it exists, so i suggest creating such a file with the following contents to overload the function defs in the stock 12.04.4 lib which don't work for you. i did some minor testing, but i'm not affected by this situation in any of the machines i use so caveat emptor.
# following functions are direct copy/pastes from
# bash_completion RELEASE: 1.99
# overloading `_longopt()' and adding `_variables()' and `_init_completion()'
# appears to be all that is needed to correct this.
#
# http://unix.stackexchange.com/questions/126300/bash-autocomplete-variables-for-other-commands-than-cd
_init_completion()
{
local exclude= flag outx errx inx OPTIND=1
while getopts "n:e:o:i:s" flag "$@"; do
case $flag in
n) exclude+=$OPTARG ;;
e) errx=$OPTARG ;;
o) outx=$OPTARG ;;
i) inx=$OPTARG ;;
s) split=false ; exclude+== ;;
esac
done
# For some reason completion functions are not invoked at all by
# bash (at least as of 4.1.7) after the command line contains an
# ampersand so we don't get a chance to deal with redirections
# containing them, but if we did, hopefully the below would also
# do the right thing with them...
COMPREPLY=()
local redir="@(?([0-9])<|?([0-9&])>?(>)|>&)"
_get_comp_words_by_ref -n "$exclude<>&" cur prev words cword
# Complete variable names.
_variables && return 1
# Complete on files if current is a redirect possibly followed by a
# filename, e.g. ">foo", or previous is a "bare" redirect, e.g. ">".
if [[ $cur == $redir* || $prev == $redir ]]; then
local xspec
case $cur in
2'>'*) xspec=$errx ;;
*'>'*) xspec=$outx ;;
*'<'*) xspec=$inx ;;
*)
case $prev in
2'>'*) xspec=$errx ;;
*'>'*) xspec=$outx ;;
*'<'*) xspec=$inx ;;
esac
;;
esac
cur="${cur##$redir}"
_filedir $xspec
return 1
fi
# Remove all redirections so completions don't have to deal with them.
local i skip
for (( i=1; i < ${#words[@]}; )); do
if [[ ${words[i]} == $redir* ]]; then
# If "bare" redirect, remove also the next word (skip=2).
[[ ${words[i]} == $redir ]] && skip=2 || skip=1
words=( "${words[@]:0:i}" "${words[@]:i+skip}" )
[[ $i -le $cword ]] && cword=$(( cword - skip ))
else
i=$(( ++i ))
fi
done
[[ $cword -eq 0 ]] && return 1
prev=${words[cword-1]}
[[ ${split-} ]] && _split_longopt && split=true
return 0
}
_variables()
{
if [[ $cur =~ ^(\$\{?)([A-Za-z0-9_]*)$ ]]; then
[[ $cur == *{* ]] && local suffix=} || local suffix=
COMPREPLY+=( $( compgen -P ${BASH_REMATCH[1]} -S "$suffix" -v -- \
"${BASH_REMATCH[2]}" ) )
return 0
fi
return 1
}
_longopt()
{
local cur prev words cword split
_init_completion -s || return
case "${prev,,}" in
--help|--usage|--version)
return 0
;;
--*dir*)
_filedir -d
return 0
;;
--*file*|--*path*)
_filedir
return 0
;;
--+([-a-z0-9_]))
local argtype=$( $1 --help 2>&1 | sed -ne \
"s|.*$prev\[\{0,1\}=[<[]\{0,1\}\([-A-Za-z0-9_]\{1,\}\).*|\1|p" )
case ${argtype,,} in
*dir*)
_filedir -d
return 0
;;
*file*|*path*)
_filedir
return 0
;;
esac
;;
esac
$split && return 0
if [[ "$cur" == -* ]]; then
COMPREPLY=( $( compgen -W "$( $1 --help 2>&1 | \
sed -ne 's/.*\(--[-A-Za-z0-9]\{1,\}=\{0,1\}\).*/\1/p' | sort -u )" \
-- "$cur" ) )
[[ $COMPREPLY == *= ]] && compopt -o nospace
elif [[ "$1" == @(mk|rm)dir ]]; then
_filedir -d
else
_filedir
fi
}
2.0-1(original version was1.3-1).shopt -s direxpandis the correct answer to solve the problem (source: askubuntu.com/questions/70750/…). At least on bash 5.0 and bash-completion 2.8 on openSUSE.