Подтвердить что ты не робот

Правильная обработка пробелов и кавычек в завершении bash

Каков правильный/лучший способ обработки пробелов и кавычек в завершении 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 может сделать это для файлов namned ‘foo’ ‘bar one’ и ‘bar two’ (хотя у него есть проблемы с некоторыми способами выражения имен файлов, когда вы используете (действительную) комбинацию как ", ' и различные экраны). Тем не менее, я не мог понять, как ls делает это, читая код завершения bash.

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

4b9b3361

Ответ 1

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

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

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

EDIT: Исправлено для обработки одиночных и двойных qoutes внутри слов. По сути, мы должны пройти 3 неадаптации:). Сначала для grep, второй для compgen и последний для команды слов, когда выполняется автозаполнение.

_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
}

Ответ 2

Я был в подобной ситуации и искал высоко и низко для ответа на это несоответствие. Вот ответ, который я придумал.

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\ ░

Учитывая ситуацию с ОП, выбор выглядит следующим образом:

  • Используйте -o filenames. Однако, если есть каталог, лежащий с тем же именем, что и соответствующее слово, завершенное слово получит произвольную косую черту в конце. (например, bar\ one/)
  • Вручную убегайте, как в ответе @Eugene, и принимайте, что кандидаты будут показаны экранированными.

Btw, есть более простой способ избежать слов, используя printf "%q":

например. 2

$ bar () {
>     foo
>     local IFS=$'\n'
>     COMPREPLY=($(printf "%q\n" "${COMPREPLY[@]}"))
> }
$ complete -F bar words
$ words ░

Tab

$ words bar\ ░

Tab Tab

bar\ one  bar\ two      # Ex.3: notice the spaces are displayed escaped
$ words bar\ ░

Ответ 3

_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 

Ответ 4

Труба _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░

tab

$ words "bar ░

tab tab

bar one  bar two
$ words "bar o░

tab

$ words "bar one" ░

Ответ 5

Я решил это, создав свою собственную функцию 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