2009-07-17 4 views
24

Каков правильный/лучший способ обработки пробелов и кавычек в завершении 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 

Typing ‘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\ ’ (хотя я предпочел бы лишать кавычек, как в этом примере, вместо их добавления).

ответ

7

Это не слишком элегантное решение для постпроцессинга, похоже, работает на меня (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 
} 
+1

Спасибо. Это решение работает для оригинальных примеров, но если я добавлю 'rock' n roll 'в words.dat, это не сработает. Мое реальное использование автозаполнения фактически включает в себя слова с апострофами, и именно по этой причине я изначально использовал 'eval'. Это достаточно легко (хотя и не очень элегантно) исправить, добавив дополнительный «поиск и замену» в цикл «for», а затем добавьте еще один цикл для строк, начинающихся с ». Единственная оставшаяся проблема, насколько я вижу, заключается в том, что автозаполнение не продвигает одну позицию курсора, если вы написали целое слово, включая любые закрывающие кавычки. –

+0

Что касается моего комментария выше. Похоже, ситуация немного хуже, чем я думал. Автозаполнение внутри слова, содержащего апострофы (например, попытка автозаполнения «рок-н-ро», написанная с использованием экранированных пространств и апострофа, или одиночные или двойные кавычки) не работает. Причина в том, что переменная 'search' не находится в ее правильной расширенной форме. Некоторые дополнительные замены кажутся возможными, но я не смог заставить это работать правильно для всех трех разных способов экранирования. –

+0

Да, compgen, кажется, зачищает все qoutes ... Что означает, что они должны быть экранированы внутри _find_words – Eugene

10

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

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 

TabTab

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

Учитывая ситуацию в OP, выбор кажется:

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

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

например 2

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

Вкладка

$ words bar\ ░ 

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

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

Ваш первый пример на самом деле не работает. TAB TAB получает ваш список в порядке, но когда вы пытаетесь «строка слов t », это терпит неудачу. – Orwellophile

+0

Вы неправильно понимаете цель примера. Если вы хотите фактически реализовать процедуру завершения bash, это требует гораздо больше, чем просто установка COMPREPLY в статический массив. Я уверен, что на SO есть более подходящие вопросы, которые касаются именно этого. – antak

+2

если и так. я взял на себя ответственность предоставить полный рабочий пример вашего примера в качестве ответа. – Orwellophile

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 
+0

Почему вы избегаете '.' в' \ .' на '$ cur'? –

+0

Потому что.- команда regex для * соответствия любому символу * – Orwellophile

+0

Но замены bash никогда не являются регулярными выражениями, * правильно? * Рассмотрим: 'text = 'foo.bar'; echo "$ {text //./_}" '- работает как буквальная замена точки на GNU bash 4.3.11 (Linux) и 3.1.20 (Windows). –

1

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

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