Каждый, кто пользовался git, docker или curl, знает приятное ощущение: набираешь начало команды, жмёшь таб, и оболочка сама предлагает варианты - подкоманды, ключи, имена файлов. Это экономит набор и память, ведь не нужно держать в голове весь список опций. А вот собственный скрипт, написанный на bash, по табу молчит. Он работает не хуже знаменитых утилит, но выглядит сыро именно из-за этой мелочи.

Между тем bash умеет дополнять команды для любого скрипта, надо лишь объяснить ему, что предлагать. Механизм называется программируемым дополнением и держится на двух встроенных командах - одна регистрирует обработчик дополнения для скрипта, другая отбирает подходящие варианты. Плюс несколько служебных переменных, через которые оболочка сообщает обработчику, что уже набрано. Разберём, как собрать дополнение от простейшего списка слов до контекстных подсказок с подкомандами и именами файлов.

Команда complete связывает скрипт с функцией-обработчиком

Сердце механизма - встроенная команда регистрации дополнения. Она говорит оболочке: когда пользователь жмёт таб после имени такого-то скрипта, вызови вот эту функцию, чтобы получить список вариантов. Функция-обработчик складывает варианты в специальный массив, откуда оболочка их и берёт.

_дополнение_мойскрипт() {
    COMPREPLY=( $(compgen -W "старт стоп статус" -- "${COMP_WORDS[1]}") )
}
complete -F _дополнение_мойскрипт мойскрипт

Разберём по частям. Функция формирует массив с зарезервированным именем - именно из него оболочка читает предложенные варианты. Команда регистрации с флагом функции связывает имя скрипта с этой функцией. После того как этот код выполнен в оболочке, нажатие таба после имени скрипта вызовет функцию, и оболочка покажет её подсказки.

Чтобы код заработал в текущем сеансе, файл с ним нужно подключить через точку или ключевое слово подключения. С этого момента таб начинает подсказывать.

. ./дополнение_мойскрипт.bash
# теперь: мойскрипт <таб> предложит старт стоп статус

Команда compgen отбирает варианты, подходящие под набранное

Вторая ключевая команда - генератор вариантов. Её задача - из полного списка возможностей отобрать те, что совпадают с уже набранным куском слова. Это избавляет от ручной фильтрации: достаточно скормить ей все варианты и текущее слово, а отбор она сделает сама.

compgen -W "сегодня завтра никогда" -- "с"
# вернёт только: сегодня - единственное совпадение на букву с

Флаг списка слов задаёт набор возможных вариантов. Двойное тире отделяет служебные опции от текущего слова, под которое идёт отбор. Команда оставляет лишь те слова из набора, что начинаются с набранного. Если набрано пустое слово, вернутся все варианты, что и даёт полный список при двойном нажатии таба.

У генератора есть и другие источники вариантов помимо списка слов. Флаг файлов предлагает имена файлов, флаг каталогов - имена каталогов. Это позволяет дополнять не только фиксированные команды, но и аргументы, требующие путь.

compgen -f -- "$слово"    # имена файлов под набранное
compgen -d -- "$слово"    # имена каталогов под набранное

Служебные переменные сообщают обработчику текущий контекст

Чтобы подсказывать осмысленно, функция должна знать, что именно набрал пользователь и где сейчас курсор. Эту картину рисуют служебные переменные дополнения, которые оболочка наполняет перед вызовом обработчика.

Массив набранных слов хранит все слова командной строки после имени скрипта. Индекс текущего слова указывает, на каком слове стоял курсор в момент нажатия таба. Вместе они дают обработчику полную ориентировку: что уже введено и какое слово сейчас дополняется.

_дополнение() {
    local текущее="${COMP_WORDS[COMP_CWORD]}"   # слово под курсором
    local предыдущее="${COMP_WORDS[COMP_CWORD-1]}" # слово перед ним
    # ... логика подсказок на основе контекста
}

Текущее слово - то, что дополняется прямо сейчас. Предыдущее слово - ключ к контекстным подсказкам, потому что по нему понятно, чего ждать дальше. Если предыдущим был ключ, требующий имя файла, логично предложить файлы. Если это была подкоманда, предложить её собственные опции.

Контекстное дополнение различает, чего ждать после каждого ключа

Простое дополнение предлагает один и тот же список всегда. Настоящая польза начинается, когда подсказки зависят от контекста. Многие ключи требуют аргумента определённого вида: после ключа ввода логично предложить файлы, после ключа каталога - каталоги. Разводит эти случаи разбор предыдущего слова через конструкцию выбора.

_дополнение_мойскрипт() {
    local текущее="${COMP_WORDS[COMP_CWORD]}"
    local предыдущее="${COMP_WORDS[COMP_CWORD-1]}"

    case "$предыдущее" in
        --файл)
            COMPREPLY=( $(compgen -f -- "$текущее") )   # предложить файлы
            return ;;
        --каталог)
            COMPREPLY=( $(compgen -d -- "$текущее") )    # предложить каталоги
            return ;;
    esac

    # по умолчанию - список ключей
    COMPREPLY=( $(compgen -W "--файл --каталог --помощь" -- "$текущее") )
}
complete -F _дополнение_мойскрипт мойскрипт

Логика читается ясно. Если предыдущее слово - ключ файла, обработчик предлагает имена файлов и завершается. Если ключ каталога - предлагает каталоги. В остальных случаях падает к списку ключей по умолчанию. Так одна функция выдаёт разные подсказки в зависимости от того, что пользователь набрал перед табом.

При дополнении именами файлов полезна команда настройки опции, помечающая варианты как файловые. Тогда оболочка обращается с ними как с путями: правильно экранирует пробелы в именах и не добавляет лишнего пробела там, где он мешает.

Подкоманды получают каждая свой набор опций

Развитые утилиты строятся вокруг подкоманд, и у каждой подкоманды свой набор ключей. Дополнение это тоже умеет: достаточно по первому слову после имени скрипта понять выбранную подкоманду и предложить её собственные опции.

_дополнение_мойскрипт() {
    local текущее="${COMP_WORDS[COMP_CWORD]}"

    if [ "$COMP_CWORD" -eq 1 ]; then
        # первое слово - сама подкоманда
        COMPREPLY=( $(compgen -W "создать удалить список" -- "$текущее") )
        return
    fi

    case "${COMP_WORDS[1]}" in
        создать)
            COMPREPLY=( $(compgen -W "--имя --шаблон" -- "$текущее") ) ;;
        удалить)
            COMPREPLY=( $(compgen -W "--принудительно --все" -- "$текущее") ) ;;
    esac
}
complete -F _дополнение_мойскрипт мойскрипт

Здесь обработчик сначала смотрит на индекс текущего слова. Если это первое слово, предлагаются сами подкоманды. Дальше по первому слову определяется выбранная подкоманда, и для неё выдаётся свой набор ключей. Так дополнение повторяет структуру утилит вроде git или docker, где у каждой подкоманды собственные опции.

Стоит предусмотреть и защиту от лишних подсказок. Если обработчик всегда смотрит на одно и то же слово, он будет предлагать одно и то же снова и снова, даже когда нужный аргумент уже введён. Проверка числа набранных слов или индекса курсора останавливает дополнение там, где предлагать больше нечего.

Постоянная установка через системный каталог дополнений

Подключение файла через точку работает лишь в текущем сеансе. Чтобы дополнение жило постоянно и подхватывалось при каждом входе в оболочку, его кладут в особый системный каталог. Bash при старте сам подключает все скрипты дополнений из него, если установлен пакет программируемого дополнения.

# Установка дополнения системно
sudo cp дополнение_мойскрипт.bash /etc/bash_completion.d/мойскрипт

После этого дополнение становится доступным во всех новых сеансах оболочки без ручного подключения. Это финальный штрих, превращающий скрипт из любительского в отполированный: он подсказывает команды наравне со штатными утилитами системы.

Что складывается в готовый рецепт

Картина собирается из нескольких кирпичей. Команда регистрации с флагом функции связывает скрипт с обработчиком. Обработчик кладёт варианты в зарезервированный массив, откуда их берёт оболочка. Генератор вариантов отбирает из полного набора те, что совпадают с набранным, и умеет брать варианты из списка слов, имён файлов или каталогов.

Служебные переменные - массив набранных слов и индекс текущего слова - дают обработчику контекст. На их основе строят контекстные подсказки через конструкцию выбора: разные варианты после разных ключей, отдельные наборы опций для разных подкоманд. Системный каталог дополнений делает всё это постоянным.

Главная мысль: дополнение - это не магия, а функция, которой оболочка сообщает контекст и от которой ждёт список подходящих слов. Стоит понять, что обработчик просто читает набранное из служебных переменных и возвращает отфильтрованный список, и вся конструкция перестаёт казаться сложной. Начинают с простого списка команд, затем наслаивают контекст - подсказки по предыдущему ключу, опции подкоманд - и собственный скрипт обретает то самое удобство, ради которого все любят таб в знакомых утилитах.