Вопрос довольно загружен, но этот ответ пытается объяснить каждый аспект:
- Как обрабатывать пробелы с
COMPREPLY
.
- Как это делает
ls
.
Есть также люди, которые задают этот вопрос и хотят знать, как их реализовать.
функция завершения в целом. Итак:
- Как мне реализовать функцию завершения и правильно установить
COMPREPLY
?
Как ls
делает это
Более того, почему он ведет себя иначе, чем когда я установил COMPREPLY
?
Еще в 12 году (до того, как я обновил этот ответ), я находился в похожей ситуации и сам искал ответ на это несоответствие. Вот ответ, который я придумал.
ls
, или, скорее, процедура завершения по умолчанию делает это с помощью функции -o filenames
. Эта опция выполняет: специфичную для имени файла обработку (например, добавление косой черты к именам каталогов или подавление завершающих пробелов .
Для демонстрации:
$ foo () { COMPREPLY=("bar one" "bar two"); }
$ complete -o filenames -F foo words
$ words ░
Tab
$ words bar\ ░ # Ex.1: notice the space is completed escaped
Tab Tab
bar one bar two # Ex.2: notice the spaces are displayed unescaped
$ words bar\ ░
Сразу же хочу пояснить два момента, чтобы избежать путаницы:
Прежде всего, ваша функция завершения не может быть реализована просто путем установки COMPREPLY
в массив вашего списка слов! В приведенном выше примере жестко задан для возврата кандидатов, начинающихся с b-a-r, просто чтобы показать, что происходит при нажатии Tab Tab . (Не волнуйтесь, мы вскоре перейдем к более общей реализации.)
Во-вторых, вышеуказанный формат для COMPREPLY
работает только потому, что указан -o filenames
. Для объяснения того, как установить COMPREPLY
, когда не используется -o filenames
, смотрите не дальше, чем следующий заголовок.
Также обратите внимание, что у -o filenames
есть и обратная сторона: если существует каталог с тем же именем, что и совпадающее слово, завершенное слово автоматически получает произвольную косую черту, присоединенную к концу. (например, bar\ one/
)
Как обрабатывать пробелы с помощью COMPREPLY
без использования -o filenames
Короче говоря, его нужно избежать.
В отличие от вышеприведенного -o filenames
демо:
$ foo () { COMPREPLY=("bar\ one" "bar\ two"); } # Notice the blackslashes I've added
$ complete -F foo words # Notice the lack of -o filenames
$ words ░
Tab
$ words bar\ ░ # Same as -o filenames, space is completed escaped
Tab Tab
bar\ one bar\ two # Unlike -o filenames, notice the spaces are displayed escaped
$ words bar\ ░
Как на самом деле реализовать функцию завершения?
Реализация функций завершения включает в себя:
- Представление вашего списка слов.
- Фильтрация списка слов только по кандидатам на текущее слово.
- Настройка
COMPREPLY
правильно.
Я не собираюсь предполагать, что знаю все сложные требования, которые могут быть для 1 и 2, а следующее - только очень базовая реализация. Я даю объяснение каждой части, чтобы можно было комбинировать и сочетать их в соответствии со своими требованиями.
foo() {
# Get the currently completing word
local CWORD=${COMP_WORDS[COMP_CWORD]}
# This is our word list (in a bash array for convenience)
local WORD_LIST=(foo 'bar one' 'bar two')
# Commands below depend on this IFS
local IFS=$'\n'
# Filter our candidates
CANDIDATES=($(compgen -W "${WORD_LIST[*]}" -- "$CWORD"))
# Correctly set our candidates to COMPREPLY
if [ ${#CANDIDATES[*]} -eq 0 ]; then
COMPREPLY=()
else
COMPREPLY=($(printf '%q\n' "${CANDIDATES[@]}"))
fi
}
complete -F foo words
В этом примере мы используем compgen
для фильтрации наших слов. (Он предоставляется bash для этой конкретной цели.) Можно использовать любое решение, которое им нравится, но я бы посоветовал не использовать grep
-подобные программы просто из-за сложности экранирования регулярного выражения.
compgen
принимает список слов с аргументом -W
и возвращает отфильтрованный результат по одному слову в строке. Поскольку наши слова могут содержать пробелы, мы заранее устанавливаем IFS=$'\n'
, чтобы при переводе результата в наш массив с синтаксисом CANDIDATES=(...)
считать только новые строки в качестве разделителей элементов.
Еще один момент, на который следует обратить внимание: аргумент -W
. Этот аргумент принимает список слов с разделителями IFS
. Опять же, наши слова содержат пробелы, поэтому это также требует IFS=$'\n'
, чтобы наши слова не разбивались
Между прочим, "${WORD_LIST[*]}"
расширяется элементами, также отделенными от того, что мы установили для IFS
, и это именно то, что нам нужно.
В приведенном выше примере я решил определить WORD_LIST
буквально в коде.
Можно также инициализировать массив из внешнего источника, такого как файл. Просто убедитесь, что вы переместили IFS=$'\n'
заранее, если слова будут разделены строкой, как в исходном вопросе:
local IFS=$'\n'
local WORD_LIST=($(cat /path/to/words.dat))`
Наконец, мы устанавливаем COMPREPLY
, чтобы избежать подобных пробелов. Экранирование довольно сложно, но, к счастью, формат printf
%q
выполняет все необходимые экранирования, которые нам нужны, и именно это мы используем для расширения CANDIDATES
. (Обратите внимание, мы говорим printf
ставить \n
после каждого элемента, потому что это то, что мы установили для IFS
.)
Наблюдатели могут заметить эту форму для COMPREPLY
применяется только в том случае, если -o filenames
не используется . Экранирование не требуется, если оно есть, и COMPREPLY
может быть установлено на то же содержимое, что и CANDIDATES
с COMPREPLY=("$CANDIDATES[@]")
.
Особую осторожность следует соблюдать, когда расширения могут выполняться для пустых массивов, поскольку это может привести к неожиданным результатам. В приведенном выше примере это выполняется путем ветвления, когда длина CANDIDATES
равна нулю.