bash и readline: завершение табуляции в цикле ввода пользователя?

Я делаю сценарий bash, который представляет пользователю командную строку.

Код cli такой:

#!/bin/bash

cmd1() {
    echo $FUNCNAME: "$@"
}

cmd2() {
    echo $FUNCNAME: "$@"
}

cmdN() {
    echo $FUNCNAME: "$@"
}

__complete() {
    echo $allowed_commands
}

shopt -qs extglob

fn_hide_prefix='__'
allowed_commands="$(declare -f | sed -ne '/^'$fn_hide_prefix'.* ()/!s/ ().*//p' | tr '\n' ' ')"

complete -D -W "this should output these words when you hit TAB"

echo "waiting for commands"
while read -ep"-> "; do
    history -s $REPLY
    case "$REPLY" in
        @(${allowed_commands// /|})?(+([[:space:]])*)) $REPLY ;;
        \?) __complete ;;
        *) echo "invalid command: $REPLY" ;;
    esac
done

Уточнение: сделано и протестировано в Bash 4

Итак, «read -e» дает возможность чтения строки, я могу вспоминать команды, редактировать строку ввода и т. Д. Что я не могу сделать, так это заставить работать завершение табуляции в readline !!

Я пробовал две вещи:

  1. Как это предположительно должно быть сделано: с помощью встроенных команд bash "complete" и "compgen", которые, как сообщается, работают здесь Обновление: сообщается, что он не работает в скриптах.

  2. Этот уродливый обходной путь

Почему readline не ведет себя правильно при использовании "complete" внутри скрипта? он работает, когда я пробую его из bash в интерактивном режиме ...


person ata    schedule 18.01.2011    source источник
comment
Мой bash (3.2) не позволяет -D complete. Какую версию bash вы используете?   -  person sorpigal    schedule 18.01.2011
comment
Я использую bash 4. Извините, я не уточнил, я обновлю сообщение.   -  person ata    schedule 18.01.2011


Ответы (5)


Попробовав собственный сценарий завершения, который, как я знаю, работает (я использую его каждый день), и столкнувшись с той же проблемой (когда он настраивал его, как ваш), я решил просмотреть исходный код bash 4.1, и нашел этот интересный блок в bash-4.1/builtins/read.def:edit_line():

old_attempted_completion_function = rl_attempted_completion_function;
rl_attempted_completion_function = (rl_completion_func_t *)NULL;
if (itext)
  {
    old_startup_hook = rl_startup_hook;
    rl_startup_hook = set_itext;
    deftext = itext;
  }
ret = readline (p);
rl_attempted_completion_function = old_attempted_completion_function;
old_attempted_completion_function = (rl_completion_func_t *)NULL;

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

РЕДАКТИРОВАТЬ. Еще кое-что: код упаковки для остановки завершения во встроенной read произошел между bash-2.05a и bash-2.05b. Я нашел эту заметку в bash-2.05b/CWRU/changelog файле этой версии:

  • edit_line (вызывается read -e) теперь просто выполняет завершение имени файла readline, устанавливая для rl_attempted_completion_function значение NULL, поскольку, например, выполнение команды для первого слова в строке было не очень полезно

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

Боюсь, у меня нет другого решения, кроме того, что вы придумали до сих пор, но, по крайней мере, мы знаем, почему оно не работает с read.

EDIT2: вот патч, который я только что протестировал, кажется, "работает". Проходит все модульные и reg-тесты и показывает эти выходные данные вашего скрипта при запуске с использованием исправленного bash, как вы и ожидали:

$ ./tabcompl.sh
waiting for commands
-> **<TAB>**
TAB     hit     output  should  these   this    when    words   you
->

Как вы увидите, я просто закомментировал эти 4 строки и некоторый код таймера для сброса rl_attempted_completion_function, когда указан read -t и наступает тайм-аут, который больше не нужен. Если вы собираетесь что-то послать Чету, вы можете сначала удалить весь rl_attempted_completion_function мусор, но это, по крайней мере, позволит вашему скрипту вести себя должным образом.

Пластырь:

--- bash-4.1/builtins/read.def     2009-10-09 00:35:46.000000000 +0900
+++ bash-4.1-patched/builtins/read.def     2011-01-20 07:14:43.000000000 +0900
@@ -394,10 +394,12 @@
        }
       old_alrm = set_signal_handler (SIGALRM, sigalrm);
       add_unwind_protect (reset_alarm, (char *)NULL);
+/*
 #if defined (READLINE)
       if (edit)
        add_unwind_protect (reset_attempted_completion_function, (char *)NULL);
 #endif
+*/
       falarm (tmsec, tmusec);
     }

@@ -914,8 +916,10 @@
   if (bash_readline_initialized == 0)
     initialize_readline ();

+/*
   old_attempted_completion_function = rl_attempted_completion_function;
   rl_attempted_completion_function = (rl_completion_func_t *)NULL;
+*/
   if (itext)
     {
       old_startup_hook = rl_startup_hook;
@@ -923,8 +927,10 @@
       deftext = itext;
     }
   ret = readline (p);
+/*
   rl_attempted_completion_function = old_attempted_completion_function;
   old_attempted_completion_function = (rl_completion_func_t *)NULL;
+*/

   if (ret == 0)
     return ret;

Имейте в виду, что пропатченный bash должен быть каким-то образом распространен или доступен везде, где люди будут использовать ваш скрипт ...

person Sdaz MacSkibbons    schedule 19.01.2011
comment
Если я правильно понимаю, завершение команд с помощью bash можно включить, закомментировав четыре строки, содержащие attempted_completion_function? - person SiegeX; 20.01.2011
comment
Возможно, но, похоже, было довольно много исправлений для утечек в read.def, некоторые ошибки, связанные с автоматическим тайм-аутом (read -t) и завершением вместе, и некоторые другие вещи в журналах изменений, а также еще несколько мест, где упоминается rl_attempted_completion_function. Я не просматривал всю кодовую базу, чтобы увидеть, поможет ли это, но попробовать стоит. Похоже, в дереве исходных текстов довольно много юнит-тестов и рег-тестов. Возможно, стоит запустить их потом :-) - person Sdaz MacSkibbons; 20.01.2011
comment
Поскольку чтение является встроенным, не обязательно предоставлять исправленный bash. Вместо этого можно было предоставить замену встроенной функции read. На мой взгляд, даже лучше было бы назвать это как-нибудь иначе, например, completing_read. Эта процедура должна быть скомпилирована для конкретной версии bash, и для этого вам понадобится исходный код bash. Не идеально, но все же лучше, чем исправлять bash. - person rocky; 04.05.2011
comment
@rocky Поправьте меня, если я ошибаюсь, но ... вышеупомянутый патч является в точности заменой (улучшением) встроенного read (который реализован на C), и он должен быть скомпилирован для конкретной версии bash .. Итак, я не вижу, чем то, что вы предлагаете, отличается от вышеизложенного. - person Sdaz MacSkibbons; 04.05.2011
comment
@Skadz Я не понял, так что позвольте мне попробовать еще раз. Необязательно предоставлять полный bash. Просто нужно предоставить встроенную функцию чтения (скажем, код ELF в GNU / Linux), которая может быть выполнена внутри вашего пакета и загружена через enable -f ‹path-to-read-builtin› read. - person rocky; 04.05.2011
comment
@Sdaz извините за неправильное написание вашего имени раньше - переполнение стека не позволяет мне исправить это, и последнее сообщение было обрезано. Допустим, я хочу упаковать это в bashdb (и могу). Людям, которые упаковывают код, нужен доступ к исходному тексту bash, а не конечным программистам, которые его устанавливают. И пакет помечен как требующий эту версию bash. В моем коде bashdb я добавляю параметр enable. Таким образом, нет никаких шансов, что другие программы, использующие read, но не нуждающиеся в завершении, будут изменены. То есть вещи полностью совместимы снизу вверх. Возможно, этот дополнительный код есть не просто так. - person rocky; 04.05.2011
comment
@rocky А, теперь я понимаю, что ты имеешь в виду. Вызов встроенных функций внешних загружаемых общих объектов, хотя теперь это имеет смысл, когда я вижу, что это возможно, бросил меня в первый раз :-) Хорошая идея. - person Sdaz MacSkibbons; 04.05.2011
comment
Теперь я понимаю проблему: первый уровень завершения в bash жестко привязан к функции завершения bash, называемой attempt_shell_completion_function. Восстановление функции, как это делает предлагаемый патч, вернет вам пользовательскую функцию завершения bash, но это не то, что я хочу в bashdb. Вместо этого я хочу, чтобы сначала выполнялись мои команды отладчика. Патч выдаст список, когда у вас ничего нет, но что произойдет, если вы начнете вводить одну из команд отладчика? Патч даст завершение команд оболочки. Не то, что я хочу. - person rocky; 05.05.2011
comment
Пересмотренный код, который я сейчас использую. Пока что вроде работает: bashdb.git.sourceforge.net/bashcdgit/git/ - person rocky; 05.05.2011
comment
Rocky, Sdaz: Я никогда не находил времени, чтобы выразить свою благодарность обоим за то, что они исследовали все это. Работа тянула меня далеко от этого весь год, но я все же хочу довести ее до конца. Я поиграю с вашим кодом, как только смогу, и надеюсь, что в конце концов Чет это получит. - person ata; 04.11.2011
comment
@rocky, как мне это построить? - person Michael Chav; 18.08.2016
comment
@MichaelChav Вы спрашиваете меня о чем-то расплывчатом и открытом, что обсуждалось 5 лет назад? Я не понимаю, о чем вы спрашиваете, и сейчас у меня все очень беспокойно, и я не могу приложить усилия прямо сейчас. Предлагаю начать новый вопрос. В нем опишите, что вы пытаетесь сделать и что уже пробовали. Здесь, вероятно, можно вырезать и вставить соответствующие части. Тогда, возможно, кто-нибудь, у кого есть время, может взглянуть на вас и помочь вам. - person rocky; 18.08.2016

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

#!/bin/bash

set -o emacs;
tab() {
  READLINE_LINE="foobar"
  READLINE_POINT="${#READLINE_LINE}"
}
bind -x '"\t":"tab"';
read -ep "$ ";

Установите параметр emacs, чтобы включить привязку клавиш, привязать клавишу табуляции к функции, изменить READLINE_LINE, чтобы обновить строку после приглашения, и установите READLINE_POINT, чтобы отразить новую большую длину строки.

В моем случае использования я фактически имитирую переменные COMP_WORDS, COMP_CWORD и COMPREPLY, но этого должно быть достаточно, чтобы понять, как добавить пользовательское завершение вкладки при использовании read -ep.

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

person muji    schedule 05.03.2013
comment
Как этот пример должен работать? Я должен запустить скрипт, нажать Tab, и я должен увидеть foobar? - person pihentagy; 08.10.2013
comment
@pihentagy: Да, но для этого требуется bash 4+. Присвоение значения READLINE_LINE заменяет редактируемую строку этим значением. - person mklement0; 16.11.2014

Что ж, похоже, я наконец-то наткнулся на ответ, и это, к сожалению, так и есть: на самом деле нет полной поддержки readline при взаимодействии с ним через read -e.

Ответ дает разработчик BASH Чет Рэми. В этой теме точно такая же проблема. адресованный:

Я пишу сценарий с помощью интерпретатора командной строки, и я могу работать с большинством вещей (например, с историей и т. Д.), За исключением одного. Автозавершение имени файла хорошо работает для некоторых команд, но я хотел бы использовать другие варианты завершения для других. Хорошо работает из реальной командной строки, но я не могу заставить ее работать должным образом в моем цикле read -e, eval ..

Вы не сможете этого сделать. `read -e 'использует только завершения строки чтения по умолчанию.

Чет

Итак, если я что-то не упустил // rant // пока bash передает программисту механизм read -e как средство для полного и правильного взаимодействия с пользователем CLI, функциональность будет нарушена, даже если базовый механизм (readline ) работает и интегрируется с остальной частью bash безупречно // конец тирады //

Я задал этот вопрос добрым людям на #bash in freenode, и мне было предложено попробовать обертку Readline, например rlfe или rlwrap.

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

ИМХО, не учитывая затраченных усилий, возможность создать полноценный интерфейс командной строки с помощью всего 5 строк кода, что-то, что можно было бы реализовать на многих языках при одном желании, является ошибкой.

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

person ata    schedule 19.01.2011
comment
Добавлен исходный патч bash-4.1, который, похоже, работает с моим ответом выше. - person Sdaz MacSkibbons; 20.01.2011

Я не уверен, что это точно отвечает на вопрос OP, но я искал, какую команду можно использовать, чтобы получить завершение вкладки bash по умолчанию для известных исполняемых команд (согласно $PATH), как показано при нажатии TAB < / kbd>. Поскольку меня впервые привели к этому вопросу (который, я думаю, связан), я подумал, что отправлю здесь заметку.

Например, в моей системе ввод lua, а затем TAB дает:

$ lua<TAB>
lua       lua5.1    luac      luac5.1   lualatex  luatex    luatools

Оказывается, есть встроенная bash (см. # 949006 команду Linux, чтобы перечислить все доступные команды и псевдонимы), называется compgen - и я могу передать ему ту же строку lua, что и в интерактивном случае, и получить такие же результаты, как если бы я нажал TAB:

$ compgen -c lua
luac
lua5.1
lua
luac5.1
luatex
lualatex
luatools

... и это именно то, что я искал :)

Надеюсь, это кому-то поможет,
Ура!

person sdaau    schedule 03.07.2012

Если вы собираетесь приложить столько усилий, почему бы просто не добавить стоимость одной или двух вилок и использовать что-то, что более чем способно предоставить все, что вы хотите. https://github.com/hanslub42/rlwrap

#!/bin/bash

which yum && yum install rlwrap
which zypper && zypper install rlwrap
which port && port install rlwrap
which apt-get && apt-get install rlwrap

REPLY=$( rlwrap -o cat )

Или, как написано на странице руководства:

В сценарии оболочки используйте rlwrap в однократном режиме вместо read

order=$(rlwrap -p Yellow -S 'Your pizza? ' -H past_orders -P Margherita -o cat)
person Orwellophile    schedule 16.05.2012
comment
+1 за упоминание rlwrap; к сожалению, я все еще использую Lucid, у которого есть версия 0.34, в которой нет опции -o (--one-shot); очевидно, что для использования этой опции требуется как минимум версия 0.35. Ваше здоровье! - person sdaau; 03.07.2012