Про bash completion на хабре я уже писал тут, и даже конце пообещал рассказать про настройку автодополнения для собственных скриптов.
Однако, прошло уже полтора года, а лично у меня до продолжения руки так и не дошли. Зато эту почетную обязанность взял на себя хабраюзер infthi, опубликую от его имени.
Рассматривать я буду модельную задачу, близкую к той, на которой я постигал азы автодополнений. Суть такова:

Есть скрипт, у которого есть три подкоманды. Одна из этих команд — work — в примере не рассматривается, у оставшихся есть следующие подкоманды. history делает import или export, каждой из этих команд надо передать имя проекта и пару значений под флагами. help может рассказать про work, history и help.
Теперь о том, как, собственно, работает автодополнение. Для баша пишется функция, которой передаются уже введенные аргументы, и на основе их она генерирует возможные варианты дополнения. Эта функция (назовём её _my_command) регистрируется для конкретной команды (в данном случае — мы исполняем скрипт названный script, поэтому регистрация идет для script) волшебной командой complete:
Теперь самое интересное — написание этой функции, обрабатывающей аргументы, и выдающей список доступных параметров.
Для начала, из man bash можно узнать о существовании специальных переменных для работы с автодополнением. Мы будем использовать следующие:
COMPREPLY
Это массив, из которого bash получает возможные дополнения.
COMP_WORDS
Это массив, содержащий уже введённые аргументы. Анализируя их, мы можем понять, какие варианты дополнения надо предлагать.
COMP_CWORD
Это — индекс в предыдущем массиве, который показывает позицию в нем аргумента, редактируемого в данный момент.
Теперь попробуем на основе этих переменных проанализировать ввод, и если вводится первый аргумент — попробовать его дополнить
если теперь мы запишем эту функцию вместе с приведенным выше вызовом complete в какой-нибудь скрипт, например ./complete.sh, выполним его в текущей консоли (лучше, конечно, для экспериментов запускать новый баш, а потом его убивать) как. ./complete sh, и, введя «script », нажмем Tab 2 раза, bash предложит нам варианты дополнения:
Соответственно, если начать вводить какую-то подкоманду, например wo и нажать Tab, то произойдёт автодополнение.
Однако я ещё не объяснил, как именно работает использованная в скрипте магия, а именно
Тут мы заполняем список возвращаемых вариантов с помощью встроенной утилиты bash compgen.
Данная утилита принимает на вход список всех возможных значений аргумента, а так же текущую введенную часть аргумента, и выбирает те значения, до которых введенную часть можно дополнить. Введенная часть аргумента передается после --, а со списком возможных значений всё интереснее. В приведенном случае, возможные значения берутся (как указывает флаг -W) и данного скрипту списка слов (т.е. в приведенном выше примере — из subcommands_1=«work history help»). Однако там можно указывать и другие флаги — например -d — и тогда compgen будет дополнять исходя из существующих на машине директорий, или -f — тогда он будет дополнять до файлов.
Можно посмотреть, что он выдаёт:
Соответственно можно генерировать различные списки кандидатов на автодополнение. Например, для решаемой задачи, нам (для импорта и экспорта истории) нужен список возможных проектов. В моём случае, каждому проекту соответствует директория в "${HOME}/projects", соответственно кандидатов можно подбирать как
Таким образом, автодополнение работает просто: смотрим на аргументы, введённые до текущего, и исходя из соображений синтаксиса вызова нужной команды генерируем список возможных значений текущего аргумента. После чего список претендентов фильтруется по уже введенной части аргумента, и передается башу. Если претендент один, то баш его и подставляет. Всё просто.
В завершение — моя топорная реализация автодополнения для модели, указанной в начале:
Ну и самое главное — подключение нашего скрипта к башу на постоянной основе. Это делается либо (топорно) прописыванием вызова нашего скрипта в .bashrc, либо (стандартно, если у вас есть такой файл: ) через /etc/bash_completion. Во втором случае мы должны положить наш скрипт в /etc/bash_completion.d, все скрипты откуда подцепляются из /etc/bash_completion.
P.S.: Напомню, что положительный фидбек стоит оставлять в карме пользователя infthi
Однако, прошло уже полтора года, а лично у меня до продолжения руки так и не дошли. Зато эту почетную обязанность взял на себя хабраюзер infthi, опубликую от его имени.
Рассматривать я буду модельную задачу, близкую к той, на которой я постигал азы автодополнений. Суть такова:

Есть скрипт, у которого есть три подкоманды. Одна из этих команд — work — в примере не рассматривается, у оставшихся есть следующие подкоманды. history делает import или export, каждой из этих команд надо передать имя проекта и пару значений под флагами. help может рассказать про work, history и help.
Теперь о том, как, собственно, работает автодополнение. Для баша пишется функция, которой передаются уже введенные аргументы, и на основе их она генерирует возможные варианты дополнения. Эта функция (назовём её _my_command) регистрируется для конкретной команды (в данном случае — мы исполняем скрипт названный script, поэтому регистрация идет для script) волшебной командой complete:
complete -F _my_command script
Теперь самое интересное — написание этой функции, обрабатывающей аргументы, и выдающей список доступных параметров.
Для начала, из man bash можно узнать о существовании специальных переменных для работы с автодополнением. Мы будем использовать следующие:
COMPREPLY
Это массив, из которого bash получает возможные дополнения.
COMP_WORDS
Это массив, содержащий уже введённые аргументы. Анализируя их, мы можем понять, какие варианты дополнения надо предлагать.
COMP_CWORD
Это — индекс в предыдущем массиве, который показывает позицию в нем аргумента, редактируемого в данный момент.
Теперь попробуем на основе этих переменных проанализировать ввод, и если вводится первый аргумент — попробовать его дополнить
_my_command(){ #объявляем функцию, которую будем привязывать к анализу
COMPREPLY=() #пока что мы не знаем, что предложить пользователю, поэтому создадим пустой список.
cur="${COMP_WORDS[COMP_CWORD]}" #получаем текущий вводимый аргумент
subcommands_1="work history help" #массив подкоманд первого уровня - см. синтаксическое дерево в начале поста.
if [[ ${COMP_CWORD} == 1 ]] ; then #если вводится первый аргумент, то попробуем его дополнить
COMPREPLY=( $(compgen -W "${subcommands_1}" -- ${cur}) ) #some magic
return 0 #COMPREPLY заполнен, можно выходить
fi
}
если теперь мы запишем эту функцию вместе с приведенным выше вызовом complete в какой-нибудь скрипт, например ./complete.sh, выполним его в текущей консоли (лучше, конечно, для экспериментов запускать новый баш, а потом его убивать) как. ./complete sh, и, введя «script », нажмем Tab 2 раза, bash предложит нам варианты дополнения:
$ script
help history work
Соответственно, если начать вводить какую-то подкоманду, например wo и нажать Tab, то произойдёт автодополнение.
Однако я ещё не объяснил, как именно работает использованная в скрипте магия, а именно
COMPREPLY=( $(compgen -W "${subcommands_1}" -- ${cur}) ) #some magic
Тут мы заполняем список возвращаемых вариантов с помощью встроенной утилиты bash compgen.
Данная утилита принимает на вход список всех возможных значений аргумента, а так же текущую введенную часть аргумента, и выбирает те значения, до которых введенную часть можно дополнить. Введенная часть аргумента передается после --, а со списком возможных значений всё интереснее. В приведенном случае, возможные значения берутся (как указывает флаг -W) и данного скрипту списка слов (т.е. в приведенном выше примере — из subcommands_1=«work history help»). Однако там можно указывать и другие флаги — например -d — и тогда compgen будет дополнять исходя из существующих на машине директорий, или -f — тогда он будет дополнять до файлов.
Можно посмотреть, что он выдаёт:
$ compgen -W "qwerty qweasd asdfgh" -- qwe
qwerty
qweasd
Соответственно можно генерировать различные списки кандидатов на автодополнение. Например, для решаемой задачи, нам (для импорта и экспорта истории) нужен список возможных проектов. В моём случае, каждому проекту соответствует директория в "${HOME}/projects", соответственно кандидатов можно подбирать как
COMPREPLY=($(compgen -W "`ls ${HOME}/projects`" -- ${cur}))
Таким образом, автодополнение работает просто: смотрим на аргументы, введённые до текущего, и исходя из соображений синтаксиса вызова нужной команды генерируем список возможных значений текущего аргумента. После чего список претендентов фильтруется по уже введенной части аргумента, и передается башу. Если претендент один, то баш его и подставляет. Всё просто.
В завершение — моя топорная реализация автодополнения для модели, указанной в начале:
_my_command()
{
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
subcommands_1="work history help" #возможные подкоманды первого уровня
subcommands_history="import export" #возможные подкоманды для history
if [[ ${COMP_CWORD} == 1 ]] ; then # цикл определения автодополнения при вводе подкоманды первого уровня
COMPREPLY=( $(compgen -W "${subcommands_1}" -- ${cur}) )
return 0
fi
subcmd_1="${COMP_WORDS[1]}" #К данному моменту подкоманда первого уровня уже введена, и мы её выбираем в эту переменную
case "${subcmd_1}" in #Дальше смотри, что она из себя представляет
work)
COMPREPLY=() #ничего дальше вводить не надо
return 0
;;
history)
if [[ ${COMP_CWORD} == 2 ]] ; then #введены script history; надо подставить import или export
COMPREPLY=( $(compgen -W "${subcommands_history}" -- ${cur}) )
return 0
fi
#к данному моменту мы уже знаем, что делаем: импорт или экспорт
subcmd_2="${COMP_WORDS[2]}"
if [[ ${COMP_CWORD} == 3 ]] ; then #но в любом случае следующим аргументом идет имя проекта.
COMPREPLY=($(compgen -W "`ls ${HOME}/projects`" -- ${cur}))
return 0
fi
case "${subcmd_2}" in #а дальше у импорта и экспорта набор флагов разный. мы смотрим на предпоследний аргумент, и если он является флагом - подставляем соответствующие ему значения, иначе - выдаем на дополнение список флагов.
import)
case "${COMP_WORDS[COMP_CWORD-1]}" in
-src)
COMPREPLY=($(compgen -d -- ${cur})) #тут должна быть директория с исходниками
return 0
;;
-file)
COMPREPLY=($(compgen -f -- ${cur})) #тут должен быть импортируемый файл
return 0
;;
*)
COMPREPLY=($(compgen -W "-src -file" -- ${cur})) #список возможных флагов
return 0
;;
esac
;;
export) #у экспорта только один флаг -o, если был он - то мы предлагаем дополнение до файла, куда экспортировать, иначе - предлагаем дополнение до флага
if [[ ${COMP_WORDS[COMP_CWORD-1]} == "-o" ]] ; then
COMPREPLY=($(compgen -f -- ${cur}))
return 0
fi
COMPREPLY=($(compgen -W "-o" -- ${cur}))
return 0
;;
*)
;;
esac
;;
help) #список возможных дополнений после help совпадает со списком подкоманд первого уровня, их и исследуем.
COMPREPLY=( $(compgen -W "${subcommands_1}" -- ${cur}))
return 0
;;
esac
return 0
}
complete -F _my_command script
Ну и самое главное — подключение нашего скрипта к башу на постоянной основе. Это делается либо (топорно) прописыванием вызова нашего скрипта в .bashrc, либо (стандартно, если у вас есть такой файл: ) через /etc/bash_completion. Во втором случае мы должны положить наш скрипт в /etc/bash_completion.d, все скрипты откуда подцепляются из /etc/bash_completion.
P.S.: Напомню, что положительный фидбек стоит оставлять в карме пользователя infthi