bash завершение табуляции с пробелами

У меня проблема с завершением bash, когда возможные варианты могут содержать пробелы.

Допустим, мне нужна функция, которая повторяет первый аргумент:

function test1() {
        echo $1
}

Я генерирую список возможных вариантов завершения (в некоторых есть пробелы, в некоторых нет), но мне не удается правильно обрабатывать пробелы.

function pink() {
    # my real-world example generates a similar string using awk and other commands
    echo "nick\ mason syd-barrett david_gilmour roger\ waters richard\ wright"
}

function _test() {
    cur=${COMP_WORDS[COMP_CWORD]}
    use=`pink`
    COMPREPLY=( $( compgen -W "$use" -- $cur ) )
}
complete -o filenames -F _test test

Когда я пробую это, я получаю:

$ test <tab><tab>
david_gilmour  nick           roger          waters
mason          richard        syd-barrett    wright
$ test r<tab><tab>
richard  roger    waters   wright

что, очевидно, не то, что я имел в виду.

Если я не назначу массив COMPREPLY, то есть только $( compgen -W "$use" -- $cur ), я заработаю, если останется только один вариант:

$ test n<tab>
$ test nick\ mason <cursor>

Но если вариантов осталось несколько, то все они печатаются в одинарных кавычках:

$ test r<tab><tab>
$ test 'roger waters
richard wright' <cursor>

Должно быть что-то не так с моей переменной COMPREPLY, но я не могу понять, что...

(запуск bash на Solaris, если это имеет значение...)


person geronimo    schedule 22.10.2014    source источник
comment
Вам нужны дискретные строки в массиве. Нравится array=("part one" "part two")   -  person amphetamachine    schedule 22.10.2014
comment
Кажется, это действительно проблема... но я не могу понять, как назначить вывод другой команды массиву, например. array=( $( echo \"part one\" \"part two\" ) ) приводит к массиву из 4 элементов, echo ${array[0]} напечатает "part .   -  person geronimo    schedule 22.10.2014
comment
Есть некоторые проблемы при заполнении аргументов для опций.   -  person jarno    schedule 22.10.2019


Ответы (4)


Если вам нужно обработать данные из строки, вы можете использовать встроенный в Bash оператор замены строки.

function _test() {
    local iter use cur
    cur=${COMP_WORDS[COMP_CWORD]}
    use="nick\ mason syd-barrett david_gilmour roger\ waters richard\ wright"
    # swap out escaped spaces temporarily
    use="${use//\\ /___}"
    # split on all spaces
    for iter in $use; do
        # only reply with completions
        if [[ $iter =~ ^$cur ]]; then
            # swap back our escaped spaces
            COMPREPLY+=( "${iter//___/ }" )
        fi
    done
}
person amphetamachine    schedule 22.10.2014
comment
Поскольку вы проверяете, совпадает ли $iter с ^$cur, пробелы также должны быть заменены местами в $cur: $cur="${use//\\ /___}". - person geronimo; 31.12.2014
comment
И последнее улучшение: если $cur уже начинается с (одинарной или двойной) кавычки, эту кавычку нужно убрать, а пробелы нужно экранировать вручную: if [[ $cur =~ ^\" || $cur =~ ^\' ]]; then cur="${cur/#\"/}"; cur="${cur/#\'/}"; cur="${cur/ /\\ }"; fi (ДО замены пробелов в $cur -- и, вероятно, это можно было бы сделать сразу) - person geronimo; 31.12.2014

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

Важно понимать проблему, заключающуюся в том, что ( $(compgen ... ) ) представляет собой массив, полученный путем разделения вывода команды compgen на символы в $IFS, которые по умолчанию представляют собой любые пробельные символы. Итак, если compgen возвращает:

roger waters
richard wright

тогда COMPREPLY будет фактически установлен в массив (roger waters richard wright), всего четыре возможных завершения. Если вместо этого вы используете ( "$(compgen ...)"), то COMPREPLY будет установлен в массив ($'roger waters\nrichard wright'), который имеет только одно возможное завершение (с новой строкой внутри завершения). Ни то, ни другое не то, что вы хотите.

Если ни одно из возможных завершений не имеет символа новой строки, вы можете договориться о том, чтобы возврат compgen был разделен на символ новой строки, временно сбросив IFS, а затем восстановив его. Но я думаю, что более элегантным решением будет просто использовать mapfile:

_test () { 
    cur=${COMP_WORDS[COMP_CWORD]};
    use=`pink`;
    ## See note at end of answer w.r.t. "$cur" ##
    mapfile -t COMPREPLY < <( compgen -W "$use" -- "$cur" )
}

Команда mapfile помещает строки, отправленные compgen в stdout, в массив COMPREPLY. (Опция -t приводит к удалению завершающей новой строки из каждой строки, что почти всегда требуется при использовании mapfile. Дополнительные параметры см. в help mapfile.)

Это не решает другую раздражающую часть проблемы, заключающуюся в преобразовании списка слов в форму, приемлемую для compgen. Поскольку compgen не допускает множественных опций -W и не принимает массив, единственным вариантом является форматирование строки таким образом, чтобы bash разбиение на слова (с кавычками и всем остальным) генерировало желаемый список. По сути, это означает добавление escape-последовательности вручную, как вы сделали в своей функции pink:

pink() {
    echo "nick\ mason syd-barrett david_gilmour roger\ waters richard\ wright"
}

Но это подвержено несчастным случаям и раздражает. Лучшее решение позволило бы напрямую указывать альтернативы, особенно если альтернативы генерируются каким-то образом. Хороший способ создания альтернатив, которые могут включать пробелы, — поместить их в массив. Имея массив, вы можете эффективно использовать формат %q printf для создания входной строки с правильными кавычками для compgen -W:

# This is a proxy for a database query or some such which produces the alternatives
cat >/tmp/pink <<EOP
nick mason
syd-barrett
david_gilmour
roger waters
richard wright
EOP

# Generate an array with the alternatives
mapfile -t pink </tmp/pink

# Use printf to turn the array into a quoted string:
_test () { 
    mapfile -t COMPREPLY < <( compgen -W "$(printf '%q ' "${pink[@]}")" -- "$2" )
}

Как написано, эта функция завершения не выводит завершения в форме, которая будет принята bash как отдельные слова. Другими словами, завершение roger waters генерируется как roger waters вместо roger\ waters. В (вероятном) случае, когда цель состоит в том, чтобы создать правильно цитируемые завершения, необходимо добавить escape-последовательности во второй раз после того, как compgen отфильтрует список завершения:

_test () {
    declare -a completions
    mapfile -t completions < <( compgen -W "$(printf '%q ' "${pink[@]}")" -- "$2" )
    local comp
    COMPREPLY=()
    for comp in "${completions[@]}"; do
        COMPREPLY+=("$(printf "%q" "$comp")")
    done
}

Примечание. Я заменил вычисление $cur на $2, так как функция, вызываемая через complete -F, получает команду как $1, а слово завершается как $2. (Предыдущее слово также передается как $3.) Кроме того, важно заключать его в кавычки, чтобы оно не разделялось на слова на пути к compgen.

person rici    schedule 22.10.2014
comment
Я бы предпочел не записывать файл в /tmp, я действительно не понимаю, зачем вам это нужно... Я генерирую свои альтернативы, используя awk, я могу добавить туда кавычки. - person geronimo; 23.10.2014
comment
@geronimo: я не рекомендую вам записывать файл в /tmp. Это был просто способ абстрагироваться от фактического построения альтернатив. На практике вы бы использовали mapfile -t pink < <(awk ...). Дело в том, что printf %q понимает цитирование bash лучше, чем вы или я, поскольку оно является частью bash, так что это проще и надежнее, чем цитирование по принципу «свернуть-свое-собственное». - person rici; 23.10.2014
comment
Хорошо я понял. Однако я не смогу использовать файл карты: -bash: mapfile: command not found :-( Думаю, мне придется объединить ваше решение с решением @amphetamachine... - person geronimo; 24.10.2014
comment
Дальнейшее тестирование показало мне следующее: ваша функция mangle выглядит очень многообещающе, но полезна только в сочетании с функцией mapfile (очевидно, только начиная с bash 4, я спрошу своего системного администратора, почему мы все еще используем bash с 2007 года). - person geronimo; 24.10.2014
comment
Это очень умно! У меня только одна проблема с его использованием. Мои завершения включают встроенное пространство, как и ожидалось, но это пространство не экранировано и не заключено в кавычки, чтобы защитить его в строке cmd, которую я составляю с помощью табуляции. Как лучше всего справиться с этим? Пока я использую mangle() { printf '"%q" ' "${@}" }. Обратите внимание на дополнительные кавычки вокруг %q. Это уместно? - person JFlo; 27.03.2019
comment
@JFlo: Нет, добавление дополнительных кавычек к строке, напечатанной с помощью %q, в некоторых случаях приведет к ошибкам цитирования. Что вам нужно сделать, так это изменить дважды: первый раз, чтобы создать аргумент для compgen, и второй раз, после того, как вы создадите массив из вывода compgen, чтобы перецитировать сгенерированные завершения. - person rici; 27.03.2019
comment
@JFlo: отредактировал это в своем ответе. - person rici; 29.03.2019
comment
@rici Спасибо за внесение этих изменений, хотя я вижу, что они немного отличаются. Было ли что-то не так с моим подходом, который, казалось бы, имел преимущество в том, что не нуждался в цикле? - person JFlo; 01.04.2019
comment
@JFlo: не совсем так. Я просто думал, что петля была более четкой и, возможно, более гибкой. - person rici; 01.04.2019

Окей, эта сумасшедшая штуковина во многом опирается на решение rici и не только полностью работает, но и цитирует любые дополнения, которые в ней нуждаются, и только.

pink() {
    # simulating actual awk output
    echo "nick mason"
    echo "syd-barrett"
    echo "david_gilmour"
    echo "roger waters"
    echo "richard wright"
}

_test() {
  cur=${COMP_WORDS[COMP_CWORD]}
  mapfile -t patterns < <( pink )
  mapfile -t COMPREPLY < <( compgen -W "$( printf '%q ' "${patterns[@]}" )" -- "$cur" | awk '/ / { print "\""$0"\"" } /^[^ ]+$/ { print $0 }' )
}

complete -F _test test

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

Подробный пример

Вот более подробная версия функции _test, чтобы она стала более понятной:

_test() {
  local cur escapedPatterns
  cur=${COMP_WORDS[COMP_CWORD]}
  mapfile -t patterns < <( pink )
  escapedPatterns="$( printf '%q ' "${patterns[@]}" )"
  mapfile -t COMPREPLY < <( compgen -W "$escapedPatterns" -- "$cur" | quoteIfNeeded )
}

quoteIfNeeded() {
  # Only if it contains spaces. Otherwise return as-is.
  awk '/ / { print "\""$0"\"" } /^[^ ]+$/ { print $0 }'
}

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

Он работает:

  1. Извлечение вывода awk в массив с использованием mapfile.
  2. Экранирование массива и помещение его в строку.
  3. Наличие одного пробела за %q в качестве маркера разделения.
  4. Цитирую $cur, Очень важно!
  5. Цитируя вывод compgen. И только если он содержит пробелы.
  6. Передача этого вывода в COMPREPLY с использованием еще одного вызова mapfile.
  7. Не использовать -o filenames.

И это только работает со всеми этими трюками. Это терпит неудачу, если хотя бы один отсутствует. Поверьте мне; Я пробовал. ;)

person Evi1M4chine    schedule 03.12.2016
comment
Возможно, было бы неплохо указать все варианты завершения, потому что в таком случае лучше будет использовать завершение «nicklas» и «nick mason» из «n». - person jarno; 22.10.2019

У меня нет mapfile в наличии. Следующее кажется немного проще, чем другие ответы, и у меня отлично сработало:

_completion() {
  local CUR=${COMP_WORDS[COMP_CWORD]}
  local OPT
  local -a OPTS

  while read -r OPT; do
    local OPT_ESC
    OPT_ESC="$(printf '%q' "$OPT")"
    [[ -z "$CUR" ]] || [[ "$OPT_ESC" == "$CUR"* ]] && \
      COMPREPLY+=("$OPT_ESC")
  done < "${TOKEN_FILE}"
}
  • Каждая строка в TOKEN_FILE является вариантом завершения.
  • Замените "${TOKEN_FILE}" на <(token_generator), где token_genorator — это какая-то команда, которая генерирует токены завершения, в режиме онлайн за раз.
  • printf '%q' дает нам строку с экранированием bash, подходящую для использования в командной строке в качестве отдельного токена.

So, if TOKEN_FILE is:

option a
another option
an option with many spaces

Затем завершение табуляции напечатает:

an\ option\ with\ many\ spaces     another\ option
option\ a
person zanerock    schedule 26.03.2021