Правильная обработка пробелов и кавычек в завершении bash - PullRequest
25 голосов
/ 18 июля 2009

Как правильно / лучше всего обрабатывать пробелы и кавычки в завершении bash?

Вот простой пример. У меня есть команда с именем words (например, программа поиска по словарю), которая принимает различные слова в качестве аргументов. Поддерживаемые слова могут содержать пробелы и определены в файле с именем words.dat:

foo
bar one
bar two

Вот мое первое предлагаемое решение:

_find_words()
{
search="$cur"
grep -- "^$search" words.dat
}

_words_complete()
{
local IFS=$'\n'

COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"

COMPREPLY=( $( compgen -W "$(_find_words)" -- "$cur" ) )

}
complete -F _words_complete words

Ввод ‘words f<tab>’ правильно завершает команду до ‘words foo ’ (с завершающим пробелом), что неплохо, но для ‘words b<tab>’ предлагается ‘words bar ’. Правильное завершение будет ‘words bar\ ’. А для ‘words "b<tab>’ и ‘words 'b<tab>’ он не предлагает никаких предложений.

Эта последняя часть, которую я смог решить. Можно использовать eval для правильного анализа (экранированных) символов. Тем не менее, eval не любит пропущенных кавычек, поэтому, чтобы все заработало, пришлось изменить search="$cur" на

search=$(eval echo "$cur" 2>/dev/null ||
eval echo "$cur'" 2>/dev/null ||
eval echo "$cur\"" 2>/dev/null || "")

Это на самом деле работает. И ‘words "b<tab>’, и ‘words 'b<tab>’ корректно автоматически заполняются, и если я добавлю ‘o’ и снова нажму <tab>, это фактически завершит слово и добавит правильную заключительную кавычку. Однако, если я пытаюсь завершить ‘words b<tab>’ или даже ‘words bar\ <tab>’, он автоматически заполняется до ‘words bar ’ вместо ‘words bar\ ’, и добавление, например, ‘one’ завершится неудачно при запуске программы words.

Теперь, очевидно, можно правильно обработать. Например, команда ls может сделать это для файлов, названных ‘foo’ ‘bar one’ и ‘bar two’ (хотя у нее действительно есть проблемы с некоторыми способами выражения имен файлов, когда используется (действительная) комбинация обоих " ' и различные побеги). Однако я не мог понять, как это делает ls, читая код завершения bash.

Так, кто-нибудь знает, как правильно справиться с этим? Фактические входные кавычки не должны быть сохранены; Я был бы счастлив с решением, которое изменяет, например, ‘words "b<tab>’, ‘words 'b<tab>’ и ‘words b<tab>’ на ‘words bar\ ’ (хотя я бы предпочел удаление кавычек, как в этом примере, вместо их добавления).

Ответы [ 5 ]

17 голосов
/ 18 июля 2012

Вопрос довольно загружен, но этот ответ пытается объяснить каждый аспект:

  1. Как обрабатывать пробелы с COMPREPLY.
  2. Как это делает ls.

Есть также люди, которые задают этот вопрос и хотят знать, как их реализовать. функция завершения в целом. Итак:

  1. Как мне реализовать функцию завершения и правильно установить 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\ ░

Как на самом деле реализовать функцию завершения?

Реализация функций завершения включает в себя:

  1. Представление вашего списка слов.
  2. Фильтрация списка слов только по кандидатам на текущее слово.
  3. Настройка 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 равна нулю.

8 голосов
/ 18 июля 2009

Мне кажется, что это не слишком элегантное решение для постобработки (GNU bash, версия 3.1.17 (6) -релиз (i686-pc-cygwin)). (Если только я не протестировал какой-нибудь пограничный случай как обычно:))

Не нужно оценивать вещи, есть только 2 вида цитат.

Поскольку compgen не хочет избегать пробелов для нас, мы сами избежим их (только если слово не начиналось с кавычки). У этого есть побочный эффект полного списка (на двойной вкладке), у которого также экранированы значения. Не уверен, хорошо это или нет, потому что ls не делает этого ...

РЕДАКТИРОВАТЬ: Исправлена ​​обработка одинарных и двойных кавычек внутри слов. По сути, мы должны пройти 3 побега :). Сначала для grep, затем для compgen и, наконец, для самой команды word, когда выполняется автозаполнение.

_find_words()
{
    search=$(eval echo "$cur" 2>/dev/null || eval echo "$cur'" 2>/dev/null || eval echo "$cur\"" 2>/dev/null || "")
    grep -- "^$search" words.dat | sed -e "{" -e 's#\\#\\\\#g' -e "s#'#\\\'#g" -e 's#"#\\\"#g' -e "}"
}

_words_complete()
{
    local IFS=$'\n'

    COMPREPLY=()
    local cur="${COMP_WORDS[COMP_CWORD]}"

    COMPREPLY=( $( compgen -W "$(_find_words)" -- "$cur" ) )

    local escaped_single_qoute="'\''"
    local i=0
    for entry in ${COMPREPLY[*]}
    do
        if [[ "${cur:0:1}" == "'" ]] 
        then
            # started with single quote, escaping only other single quotes
            # [']bla'bla"bla\bla bla --> [']bla'\''bla"bla\bla bla
            COMPREPLY[$i]="${entry//\'/${escaped_single_qoute}}" 
        elif [[ "${cur:0:1}" == "\"" ]] 
        then
            # started with double quote, escaping all double quotes and all backslashes
            # ["]bla'bla"bla\bla bla --> ["]bla'bla\"bla\\bla bla
            entry="${entry//\\/\\\\}" 
            COMPREPLY[$i]="${entry//\"/\\\"}" 
        else 
            # no quotes in front, escaping _everything_
            # [ ]bla'bla"bla\bla bla --> [ ]bla\'bla\"bla\\bla\ bla
            entry="${entry//\\/\\\\}" 
            entry="${entry//\'/\'}" 
            entry="${entry//\"/\\\"}" 
            COMPREPLY[$i]="${entry// /\\ }"
        fi
        (( i++ ))
    done
}
4 голосов
/ 13 декабря 2013
_foo ()
{
  words="bar one"$'\n'"bar two"
  COMPREPLY=()
  cur=${COMP_WORDS[COMP_CWORD]}
  prev=${COMP_WORDS[COMP_CWORD-1]}
  cur=${cur//\./\\\.}

  local IFS=$'\n'
  COMPREPLY=( $( grep -i "^$cur" <( echo "$words" ) | sed -e 's/ /\\ /g' ) )
  return 0
}

complete -o bashdefault -o default -o nospace -F _foo words 
1 голос
/ 17 июля 2015

Труба _find_words - sed и каждая строка заключена в кавычки. При вводе командной строки обязательно ставьте либо ", либо ' перед словом, которое нужно завершить, иначе этот метод не будет работать.

_find_words() { cat words.dat; }

_words_complete()
{

  COMPREPLY=()
  cur="${COMP_WORDS[COMP_CWORD]}"

  local IFS=$'\n'
  COMPREPLY=( $( compgen -W "$( _find_words | sed 's/^/\x27/; s/$/\x27/' )" \
                         -- "$cur" ) )

}

complete -F _words_complete words

Командная строка:

$ words "ba░

Вкладка

$ words "bar ░

Вкладка Вкладка

bar one  bar two
$ words "bar o░

вкладка

$ words "bar one" ░
0 голосов
/ 07 марта 2017

Я решил эту проблему, создав собственную функцию compgen2, которая обрабатывает дополнительную обработку, когда текущее слово не начинается с символа кавычки. в противном случае он работает аналогично compgen -W.

compgen2() {
    local IFS=$'\n'
    local a=($(compgen -W "$1" -- "$2"))
    local i=""
    if [ "${2:0:1}" = "\"" -o "${2:0:1}" = "'" ]; then
        for i in "${a[@]}"; do
            echo "$i"
        done
    else
        for i in "${a[@]}"; do
            printf "%q\n" "$i"
        done
    fi
}

_foo() {
    local cur=${COMP_WORDS[COMP_CWORD]}
    local prev=${COMP_WORDS[COMP_CWORD-1]}
    local words=$(cat words.dat)
    local IFS=$'\n'
    COMPREPLY=($(compgen2 "$words" "$cur"))
}

echo -en "foo\nbar one\nbar two\n" > words.dat
complete -F _foo foo
...