Фокусы с автодополнением в bash

После посещения заметки «Создание списка имен хостов для ssh на лету для автодополнения» долго вчитывался в man bash с целью понять, как же там это автодополнение работает. В конце концов, поняв, что с наскоку эту гремучую смесь из фич баша и возможностей readline не осилить, скачал оба исходника и нашел там великое множество примеров использования автозаполнения в bash, значительно упрощающих повседневную работу в консоли. Эти команды можно добавить в ~/.bash_profile или вынести в отдельный файл ~/.bash_completions или /etc/bash_completions и подключать его в .bash_profile. Список текущих дополнений можно посмотреть по команде complete (без параметров).

Данные скрипты скопированы почти без изменений из папки examples/complete архива с исходными текстами bash. Автор всех скриптов, за исключением последнего — Ian Macdonald. На его странице доступен архив с огромным количеством скриптов автодополнения. Автор последнего скрипта (автодополнение длинных опций у configure) — Manu Rouat. Большое им человеческое спасибо 🙂

Для команд работы с каталогами tab показывает только каталоги:

complete -d cd mkdir rmdir pushd

Для команд работы с файлами показываются только файлы (все или определённых типов, в зависимости от команды):

complete -f cat less more chown ln strip
complete -f -X '*.gz' gzip
complete -f -X '*.Z' compress
complete -f -X '!*.+(Z|gz|tgz|Gz)' gunzip zcat zmore
complete -f -X '!*.Z' uncompress zmore zcat
complete -f -X '!*.+(gif|jpg|jpeg|GIF|JPG|bmp)' ee xv
complete -f -X '!*.+(ps|PS|ps.gz)' gv
complete -f -X '!*.+(dvi|DVI)' dvips xdvi dviselect dvitype
complete -f -X '!*.+(pdf|PDF)' acroread xpdf
complete -f -X '!*.texi*' makeinfo texi2dvi texi2html
complete -f -X '!*.+(tex|TEX)' tex latex slitex
complete -f -X '!*.+(mp3|MP3)' mpg123

Для команд работы с заданиями показываются номера заданий, предваряемые символом «%»:

complete -A signal kill -P '%'
complete -A stopped -P '%' bg
complete -j -P '%' fg jobs disown

Сетевым командам подставляется список хостов из файла, заданного в переменной окружения HOSTFILE. Обычно это файл /etc/hosts:

complete -A hostname ssh rsh telnet rlogin ftp ping fping host traceroute nslookup

По уже упоминавшейся ссылке рекомендуют подставлять команде ssh список хостов из ~/.ssh/known_hosts:

complete -W "$(echo `cat ~/.ssh/known_hosts | cut -f 1 -d ' ' | sed -e s/,.*//g | uniq | grep -v "["`;)" ssh

Для ряда встроенных команд bash, а также системных команд тоже свои списки дополнений:

# подставляются переменные окружения
complete -v export local readonly unset
# подставляются параметры команд set, shopt, help, unalias, bind
complete -A setopt set
complete -A shopt shopt
complete -a unalias
complete -A binding bind

Для команд управления выполнением программ подставляются названия возможных программ:

complete -c command time type nohup exec nice eval strace gdb

Думаю, основной принцип понятен — указываем ключевое слово «complete», за ним некий список возможных «подстав» и далее команды, для которых этот список работает. Список подстановок может быть задан в виде уже готовой опции, как в большинстве вышеприведённых примеров (в фрагменте complete -d cd mkdir rmdir pushd опция -d означает, что должен подставляться список доступных каталогов; на самом деле это сокращённый вид опции -A directory. За списком возможных параметров опции -A обращайтесь к man builtin), в виде списка возможных слов, как в примере с .known_hosts (тогда список предваряется опцией -W), или в виде списка файлов (-X «*.gz» покажет все доступные файлы, которые не совпадают с шаблоном «*gz»; чтобы, наоборот, показать все файлы *.gz, нужно в начало шаблона добавить восклицательный знак).

Конечно же, этим возможности автодополнения bash не ограничиваются. Можно использовать свои функции, которые сработают после того, как пользователь напишет команду, но перед тем, как будет нажата кнопка Tab, показывающая список возможных параметров. Такие функции должны произвести какие-то действия для нахождения подходящих параметров для автодополнения и заполнить этими параметрами массив COMPREPLY; при этом доступны массив COMP_WORDS, содержащий все слова из уже написанной строки, переменные COMP_CWORD (номер текущего слова в массиве COMP_WORDS), COMP_LINE (вся строка целиком в одной переменной) и еще парочка (см. man bash).

Формат команды настройки автодополнения в случае использования функции следующий:

_function_name ()
{
...
}
complete -F _function_name command

Чтобы не быть голословным, приведу несколько примеров, из которых всё станет ясно:

Для команды umount подставляется список примонтированных файловых систем:

_umount ()
{
    local cur
    COMPREPLY=()
    cur=${COMP_WORDS[COMP_CWORD]}
 
    COMPREPLY=( $( mount | cut -d' ' -f 3 | grep ^$cur) )
    return 0
}
complete -F _umount umount

Для команд работы с группами пользователей подставляются названия групп из /etc/group:

_gid_func ()
{
    local cur
    COMPREPLY=()
    cur=${COMP_WORDS[COMP_CWORD]}
    COMPREPLY=( $( awk 'BEGIN {FS=":"} {if ($1 ~ /^'$cur'/) print $1}' /etc/group ) )
    return 0
}
 
complete -F _gid_func groupdel groupmod

А теперь те две функции, ради которых, собственно, и затевался этот пост 🙂
Автодополнение параметров для команды find:

_find ()
{
    local cur prev
 
        COMPREPLY=()
        cur=${COMP_WORDS[COMP_CWORD]#-}
        prev=${COMP_WORDS[COMP_CWORD-1]}
 
    case "$prev" in
    -@(max|min)depth)
        COMPREPLY=( $( compgen -W '0 1 2 3 4 5 6 7 8 9' ) )
        return 0
        ;;
    -?(a)newer|-fls|-fprint?(0|f))
        COMPREPLY=( $( compgen -f $cur ) )
        return 0
        ;;
    -fstype)
        # this is highly non-portable (the option to -d is a tab)
        COMPREPLY=( $( cut -d'  ' -f 2 /proc/filesystems | grep ^$cur ) )
        return 0
        ;;
    -gid)
        COMPREPLY=( $( awk 'BEGIN {FS=":"} 
                {if ($3 ~ /^'$cur'/) print $3}' /etc/group ) )
        return 0
        ;;
    -group)
        COMPREPLY=( $( awk 'BEGIN {FS=":"} 
                {if ($1 ~ /^'$cur'/) print $1}' /etc/group ) )
        return 0
        ;;
    -?(x)type)
        COMPREPLY=( $( compgen -W 'b c d p f l s' $cur ) )
        return 0
        ;;
    -uid)
        COMPREPLY=( $( awk 'BEGIN {FS=":"} 
                {if ($3 ~ /^'$cur'/) print $3}' /etc/passwd ) )
        return 0
        ;;
    -user)
        COMPREPLY=( $( compgen -u $cur ) )
        return 0
        ;;
    -[acm]min|-[acm]time|-?(i)?(l)name|-inum|-?(i)path|-?(i)regex| 
    -links|-perm|-size|-used|-exec|-ok|-printf)
        # do nothing, just wait for a parameter to be given
        return 0
        ;;
    esac
 
    # complete using basic options ($cur has had its dash removed here,
    # as otherwise compgen will bomb out with an error, since it thinks
    # the dash is an option to itself)
    COMPREPLY=( $( compgen -W 'daystart depth follow help maxdepth 
            mindepth mount noleaf version xdev amin anewer atime 
            cmin cnewer ctime empty false fstype gid group ilname 
            iname inum ipath iregex links lname mmin mtime name 
            newer nouser nogroup perm regex size true type uid 
            used user xtype exec fls fprint fprint0 fprintf ok 
            print print0 printf prune ls' $cur ) )
 
    # this removes any options from the list of completions that have
    # already been specified somewhere on the command line.
    COMPREPLY=( $( echo "${COMP_WORDS[@]}-" | 
               (while read -d '-' i; do
                [ "$i" == "" ] && continue
                # flatten array with spaces on either side,
                # otherwise we cannot grep on word boundaries of
                # first and last word
                COMPREPLY=" ${COMPREPLY[@]} "
                # remove word from list of completions
                COMPREPLY=( ${COMPREPLY/ ${i%% *} / } )
                done
                echo ${COMPREPLY[@]})
          ) )
 
    # put dashes back
    for (( i=0; i < ${#COMPREPLY[@]}; i++ )); do
        COMPREPLY[i]=-${COMPREPLY[i]}
    done
 
    return 0
}
complete -F _find find

Гвоздь программы! Автодополнение параметров для команды configure:

_longopt_func ()
{
    case "$2" in
    -*) ;;
    *)  return ;;
    esac
 
    case "$1" in
    ~*)    eval cmd=$1 ;;
    *)  cmd="$1" ;;
    esac
    COMPREPLY=( $("$cmd" --help | sed  -e '/--/!d' -e 's/.*--([^ ]*).*/--1/'| 
grep ^"$2" |sort -u) )
}
 
complete  -o default -F _longopt_func wget bash configure
Метки: , , . Закладка Постоянная ссылка.

Обсуждение закрыто.