【问题标题】:bash and readline: tab completion in a user input loop?bash 和 readline:用户输入循环中的制表符完成?
【发布时间】:2011-06-11 05:35:52
【问题描述】:

我正在制作一个 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 功能,我可以调用命令、编辑输入行等。我不能做任何事情是让 readline 的制表符完成工作!

我尝试了两件事:

  1. 应该如何完成:使用 bash 内置“完成”和“compgen”,据报道可以工作here 更新:据报道它不能在脚本中工作

  2. This ugly workaround

为什么在脚本中使用“完成”时 readline 的行为不正确?当我在交互模式下从 bash 尝试它时它可以工作......

【问题讨论】:

  • 我的 bash (3.2) 不允许 -Dcomplete。您使用的是哪个版本的 bash?
  • 我正在使用 bash 4。对不起,我没有澄清这一点,我会更新帖子。

标签: bash bash-completion


【解决方案1】:

在尝试了一个我知道有效的自定义完成脚本(我每天都使用它)并遇到了同样的问题(在与你的类似的情况下安装它时),我决定窥探 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-hacking 长胡子可能知道。因此,使用内置的read 执行此操作可能只是被硬编码为禁用。

编辑:更多内容:在 bash-2.05a 和 bash-2.05b 之间发生了在 read 内置函数中停止完成的包装代码。我在那个版本的bash-2.05b/CWRU/changelog 文件中找到了这个注释:

  • edit_line(由 read -e 调用)现在只是通过将 rl_attempted_completion_function 设置为 NULL 来完成 readline 的文件名完成,因为例如,对行上的第一个单词执行命令完成并不是很有用

我认为这是一个遗留的疏忽,而且由于可编程完成已经走过了漫长的道路,你所做的事情是有用的。也许你可以要求他们重新添加它,或者只是自己修补它,如果这对你正在做的事情是可行的。

恐怕除了您目前提出的解决方案之外,我没有其他解决方案,但至少我们知道为什么它不适用于 read

EDIT2:对,这是我刚刚测试的一个似乎“有效”的补丁。通过所有单元和注册测试,并在使用修补的 bash 运行时显示脚本的输出,如您所料:

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

正如您将看到的,我只是注释掉了这 4 行和一些计时器代码,以在指定 read -t 并发生超时时重置 rl_attempted_completion_function 并且不再需要超时。如果您要向 Chet 发送一些东西,您可能希望首先删除整个 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 必须以某种方式分发或以某种方式在人们使用您的脚本的任何地方提供...

【讨论】:

  • 如果我没看错的话,可以通过注释掉包含 attempted_completion_function 的四行来启用使用 bash 的命令完成?
  • 由于 read 是内置的,因此不必严格提供已修补的 bash。相反,可以提供一个替换的 read 内置函数。在我看来,更好的方法是将其称为不同的名称,例如 completing_read。该例程必须针对特定版本的 bash 进行编译,为此您需要 bash 源代码。不理想,但仍然比修补 bash 更好。
  • @Skadz 我想我不清楚,所以让我再试一次。不必提供完整的 bash。只需要提供 read 内置函数(比如 GNU/Linux 上的 ELF 代码),它可以在你的包中完成并通过 enable -f read 加载。
  • 我现在明白了问题所在:bash 中的第一级补全被硬连接为 bash 的补全函数attempt_shell_completion_function。按照建议的补丁恢复功能,将返回自定义 bash 完成功能,但这不是我在 bashdb 中想要的。相反,我希望首先完成 my 调试器命令。当你什么都没有时,补丁会给出一个列表,但是如果你开始填写其中一个调试器命令会发生什么?该补丁将提供 shell 命令完成。不是我想要的。
  • 我现在使用的修改后的代码。到目前为止,它似乎工作:bashdb.git.sourceforge.net/git/gitweb.cgi?p=bashdb/…
【解决方案2】:

一段时间以来,我一直在为同样的问题苦苦挣扎,我认为我有一个可行的解决方案,在我的现实世界中,我使用 compgen 来生成可能的完成。但这里有一个说明核心逻辑的例子:

#!/bin/bash

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

设置emacs选项以启用键绑定,将tab键绑定到函数,更改READLINE_LINE以在提示后更新该行,并设置READLINE_POINT以反映该行新的更长的长度。

在我的用例中,我实际上模仿了 COMP_WORDS, COMP_CWORDCOMPREPLY 变量,但这应该足以了解如何在使用 read -ep 时添加自定义选项卡完成。

您必须更新 READLINE_LINE 以更改提示行(完成单匹配),在提示之前打印到 stdin 打印,因为 readline 已将终端置于原始模式并正在捕获输入。

【讨论】:

  • 这个例子应该如何工作?我应该运行脚本,按 Tab,我应该看到 foobar?
  • @pihentagy:是的,但它需要 bash 4+。为 READLINE_LINE 分配一个值会将正在编辑的行替换为该值。
【解决方案3】:

好吧,看来我终于难住了答案,可悲的是:实际上通过“read -e”连接它时没有完全支持 readline。

答案由 BASH 维护者 Chet Ramey 给出。在this thread 中解决了完全相同的问题:

我正在用命令行解释器编写脚本,我可以做大部分事情 工作(例如历史等),除了一件事。文件名补全 适用于某些命令,但我想使用其他完成 其他人的选择。从“真实”命令行运行良好,但我不能 让它在我的“read -e, eval”循环中正常工作..

你将无法做到这一点。 `read -e' 仅使用 readline 默认值 完成。

切特

所以,除非我遗漏了一些东西 //rant// 当 bash 将“read -e”机制作为完整、正确的 CLI 用户接口的手段交给程序员时,该功能被削弱了,即使底层机制(readline)工作并与 bash 的其余部分完美集成 //end rant//

我已在 freenode 的 #bash 中向好心人公开了这个问题,并被建议尝试使用像 rlferlwrap 这样的 Readline 包装器。

最后,我昨天通过邮件联系了切特本人,他确认这是设计使然,并且他不想将其作为可编程完成的唯一用例更改为“读取”,即显示一个列表脚本用户的命令,看起来不像是花时间在这方面工作的令人信服的理由。不过他表示,万一有人真的去做了,他肯定会看结果的。

恕我直言,不考虑仅用 5 行代码构建完整 CLI 的努力的价值,这是一个错误。

在这种情况下,我认为西蒙的回答非常出色且正确。我会尝试按照您的步骤进行操作,也许运气好的话,我会获得更多信息。然而,自从我不使用 C 语言以来已经有一段时间了,我认为我必须掌握的代码量才能实现不会是微不足道的。但无论如何我会尝试的。

【讨论】:

  • 添加了一个 bash-4.1 源代码补丁,似乎对我上面的答案有效。
【解决方案4】:

我不确定这是否准确回答了 OP 问题 - 但我正在搜索可以使用哪个命令,以获得已知可执行命令的默认 bash 选项卡完成(根据 $PATH),如图所示按 TAB 时。自从我第一次被引导到这个问题(我认为这是相关的),我想我会在这里张贴一个说明。

例如,在我的系统上,输入 lua 然后 TAB 给出:

$ lua
lua lua5.1 luac luac5.1 lualatex luatex luatools

事实证明,有一个内置的bash(参见#949006 Linux command to list all available commands and aliases),称为compgen - 我可以使用与交互式案例中相同的字符串lua 来输入它,并获得相同的结果好像我按了 TAB:

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

...这正是我要找的:)

希望这对某人有所帮助,
干杯!

【讨论】:

    【解决方案5】:

    如果您要付出那么多努力,为什么不增加一两个叉子的成本,并使用能够提供您想要的一切的东西。 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 )
    

    或者正如手册页所说:

    在 shell 脚本中,在“one-shot”模式下使用 rlwrap 替代 read

    order=$(rlwrap -p Yellow -S 'Your pizza? ' -H past_orders -P Margherita -o cat)
    

    【讨论】:

    • +1 用于提及rlwrap;不幸的是,我仍在使用 0.34 版本的 Lucid,它没有 -o (--one-shot) 选项;显然至少需要 v. 0.35 才能使用该选项。干杯!
    猜你喜欢
    • 2016-05-20
    • 1970-01-01
    • 2019-06-18
    • 1970-01-01
    • 1970-01-01
    • 2014-01-02
    • 2014-12-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多