是否可以对 bash 自动完成输出进行分类?

Is it possible to categorize the bash autocomplete output?

我已阅读bash programmable completion on the GNU website, and many good Q&A on Whosebug and unix.stackexchange.com。幸运的是,我制作了一个自动完成脚本,它几乎完全符合我的要求。但是,我需要对建议进行格式化。

有没有办法根据建议的类型对建议进行分类。例如,我想要这样的东西:

$ foo --bar # press TAB twice and get the result as below:
* Directories:
foo/   bar/   baz/
* Files:
foo.sh   bar.sh   baz.sh
* Options:
--foo=   --bar   --baz

而不是这个:

$ foo --bar # press TAB twice and get the result as below:
--bar      --baz      --foo=      bar/      baz/
foo/       bar.sh     baz.sh      foo.sh

完成脚本的最小代码片段为:

_foo(){

   COMPREPLY=()
   local word=""
   local prev=""

   # Suggest directories
   COMPREPLY+=( $( compgen -d -- $word ) )
   
   # Suggest '*.sh' files with negating the '-X' filter pattern
   COMPREPLY+=( $( compgen -f -X "![.][sS][hH]$" -- $word ) )

   # Suggest options in the '-W' word list
   COMPREPLY+=( $( compgen -W "--foo= --bar --baz" -- $word ) )
}

complete -F _foo foo

感谢您的时间、想法和见解:)

更新:

我调整了@Socowi 的 ,这样额外的 \ 就不会出现在 = 标志之前。

compgenSection() {
  title="* :"
  shift
  local entries
  mapfile -t entries < <(compgen "$@")
  (( "${#entries[@]}" == 0 )) && return
  COMPREPLY_SECTIONLESS+=("${entries[@]}")
  mapfile -tO "${#COMPREPLY[@]}" COMPREPLY < <(
    printf "%$((-COLUMNS/2))s\n" "$title"
    printf %s\n "${entries[@]}" | sort | column -s $'\n' | expand
  )
  [[ "${COMPREPLY[@]}" =~ "=" ]] && compopt -o nospace
}   
_foo(){
  local word=""
  COMPREPLY_SECTIONLESS=()
  compgenSection Directories -d -S / -- "$word"
  compgenSection Files -f -X '!*.[sS][hH]' -- "$word"
  compgenSection Options -W '--foo= --bar --baz' -- "$word"
  (( "${#COMPREPLY_SECTIONLESS[@]}" <= 1 )) &&
  COMPREPLY=("${COMPREPLY_SECTIONLESS[@]}")
}
complete -o nosort -F _foo foo

我不能确定自定义格式是否不可能,但如果是的话,这里有一个可能的解决方法以防万一:

将章节标题添加到 COMPREPLY 以完全显示它们。现在我们还有其他问题需要解决:

  1. 完成排序。章节标题出现在错误的位置。
    解决方法: 使用 complete -o nosort ... 以正确的顺序显示章节标题和条目。您可以再次手动对每个部分中的条目进行排序。
  2. 由于 bash 在多列中显示完成,因此部分标题可能与其他条目出现在同一行中。
    解决方法: 强制每个完成条目显示在其自己的行中。 Bash 将 COMPREPLY 中的条目格式化为大小均匀的列。如果其中一列的宽度超过终端的一半,则所有条目将显示在各自的行中。只需用空格填充章节标题即可。
  3. 现在所有完成也都显示在单行中,但我们希望它们显示在列中。
    解决方法: 手动将所有完成项放入列中。然后将每一行(可能有多个条目)添加为 COMPGEN.
  4. 中的单个条目
  5. 按一次 tab 自动完成不起作用。由于章节标题,总是存在不止一种可能性。
    解决方法: 确保在 word= 更改时相应地调整 COMPREPLY。如果在所有部分中只有一个匹配项,则必须取消 headers 部分,以便 COMPREPLY 仅包含该单个匹配项。在这里我们还抑制了空白部分,使它看起来更漂亮。

实施

compgenSection() {
  title="* :"
  shift
  local entries
  mapfile -t entries < <(compgen "$@")
  (( "${#entries[@]}" == 0 )) && return
  COMPREPLY_SECTIONLESS+=("${entries[@]}")
  mapfile -tO "${#COMPREPLY[@]}" COMPREPLY < <(
    printf "%$((-COLUMNS/2))s\n" "$title"
    printf %s\n "${entries[@]}" | sort | column -s $'\n' | expand
  )
}   
_foo(){
  local word=""
  COMPREPLY_SECTIONLESS=()
  compgenSection Directories -d -- "$word"
  compgenSection Files -f -X '!*.[sS][hH]' -- "$word"
  compgenSection Options -W '--foo= --bar --baz' -- "$word"
  (( "${#COMPREPLY_SECTIONLESS[@]}" <= 1 )) &&
  COMPREPLY=("${COMPREPLY_SECTIONLESS[@]}")
}
complete -o nosort -o filenames -F _foo foo

请注意,我必须更改您的 compgen -f -X "![.][sS][hH]$",因为 compgen 接受的模式是 glob 而不是正则表达式。

我还解决了 array=( $(compgen ...) ) 的问题,其中带空格的文件名被拆分为多个完成条目。对于其中包含换行符的文件名,这仍然会发生。然而,这似乎是正常行为:尝试 touch $'b\na'; cat btabtab,它打印 a b 而不是 $'b\na'.

我还添加了选项 complete -o filenames,以便在 auto-completion 上正确引用其中包含空格的条目。如果这干扰了选项的完成(您可能需要实际空间),您可以 运行 通过 printf %q 手动输入文件和目录条目以仍然获得正确的引用。

测试

以下是互动 session 中的一些示例。
⌨️ 是提示符。 标记我按下 tab 的位置。

⌨️ mkdir /tmp/test/{,foo,bar,baz}; cd /tmp/test; touch foo.sh bar.sh baz.sh
⌨️ foo ↹↹
* Directories:                          
bar     baz     foo
* Files:                                
bar.sh  baz.sh  foo.sh
* Options:                              
--bar   --baz   --foo=
⌨️ foo f↹↹
* Directories:                          
foo
* Files:                                
foo.sh
⌨️ foo foo.↹ # auto-completes to `foo foo.sh`