34

我正在制作一个 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”,据报道可以在这里工作 更新:据报道它不能在脚本中工作

  2. 这个丑陋的解决方法

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

4

5 回答 5

25

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

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

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

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

恐怕除了您到目前为止提出的解决方案之外,我没有其他解决方案,但至少我们知道为什么它不适用于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并发生超时,这不再需要。如果您要向 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 必须以某种方式分发或以某种方式提供,无论人们将使用您的脚本...

于 2011-01-19T17:36:46.657 回答
17

我一直在为同样的问题苦苦挣扎一段时间,我认为我有一个可行的解决方案,在我的现实世界中,我使用 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_CWORDandCOMPREPLY变量,但这应该足以理解如何在使用read -ep.

You must update READLINE_LINE to change the prompt line (completion single match), printing to stdin prints before the prompt as readline has put the terminal in raw mode and is capturing input.

于 2013-03-05T09:46:44.087 回答
12

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

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

我正在用命令行解释器编写一个脚本,除了一件事之外,我可以做大多数事情(例如历史等)。文件名完成对于某些命令效果很好,但我想为其他命令使用其他完成选项。从“真实”命令行运行良好,但我无法让它在我的“read -e,eval”循环中正常运行..

你将无法做到。`read -e' 只使用 readline 默认补全。

切特

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

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

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

恕我直言,不考虑仅用 5 行代码就可以实现完整 CLI 的能力是一个错误,这是一个错误。

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

于 2011-01-19T22:02:53.370 回答
2

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

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

$ lua <TAB>
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

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

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

于 2012-07-03T18:03:08.550 回答
1

如果您要付出那么多努力,为什么不增加一两个叉子的成本并使用能够提供您想要的一切的东西。 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 脚本中,使用rlwrap'one-shot' 模式代替read

order=$(rlwrap -p Yellow -S 'Your pizza? ' -H past_orders -P Margherita -o cat)
于 2012-05-16T02:05:49.737 回答