Bash,使用case语句检查单词是否在数组中

Bash,使用case语句检查单词是否在数组中

我正在编写一个脚本,它必须接受有限的预定义列表中的单词作为参数。我也希望它能完成。我将 list 存储在变量中以避免complete和之间的重复case。所以我写了这个,完成确实有效,但case声明不起作用。为什么?不能只用变量来制作 case 语句参数吗?

declare -ar choices=('foo' 'bar' 'baz')
function do_work {
  case "$1" in 
    "${choices[*]}")
      echo 'yes!'
      ;;
    *)
      echo 'no!'
  esac
}
complete -W "${choices[*]}" do_work

答案1

中的列表complete -W list被解释为$IFS分隔列表,并$IFS考虑完成时的 。

所以如果你有:

complete -W 'a b,c d' do_work

do_work当包含空格时,完成将提供a,b,c以及当包含且不包含空格 时。d$IFSa bc d$IFS,

所以它主要是被设计破坏的。它也不允许提供任意字符串作为补全。

有了这些限制,您能做的最好的事情就是假设$IFS永远不会被修改(因此将始终包含空格、制表符和换行符,因此不能在完成词中使用字符),然后执行以下操作:

choices='foo bar baz'
do_work() {
  case "$1" in
    (*' '*) echo 'no!';;
    (*)
      case " $choices " in 
        (*" $1 "*) echo 'yes!';;
        (*) echo 'no!';;
      esac;;
  esac
}
complete -W "$choices" do_work

您可以添加 areadonly IFS以确保$IFS永远不会被修改,但这可能会破坏事情,特别是考虑到它bash不允许您声明已在父作用域中声明为只读的局部变量,因此即使这样做的函数local IFS=,也会破坏。

至于如何检查是否在数组元素中找到字符串的更一般问题,bash(与 zsh 相反)没有运算符,但是,您可以轻松地用循环实现它:

amongst() {
  local string needle="$1"
  shift
  for string do
    [[ $needle = "$string" ]] && return
  done
  false
}

进而:

do_work {
  if amongst "$1" "${choices[@]}"; then
    echo 'yes!'
  else
    echo 'no!'
  fi
}

查找字符串更合适的结构是使用哈希表或关联数组:

typeset -A choices=( [foo]=1 [bar]=1 [baz]=1 )
do_work() {
  if [[ -n ${choices[+$1]} ]]; then
    echo 'yes!'
  else
    echo 'no!'
  fi
}
complete -W "${!choices[*]}" do_work

这里将"${!choices[*]}"关联数组的键与$IFS该点的第一个字符连接起来(如果设置但为空,则不使用分隔符)。

请注意,bash 关联数组不能有空键,因此空字符串不能成为选择之一,但无论如何complete -W也不支持这一点,并且完成空字符串无论如何都不是很有用,除了可能用于完成列表之外向用户显示它是可接受的值之一。

答案2

语句中的值case必须用显式分隔|,因为|is 是语法的一部分。但是,由于 case 语句中的值必须是模式,因此可以使用扩展模式。

生成string="*@($(IFS="|";echo "${choices[*]}"))*"并将其用作扩展模式,如下所示:

#!/bin/bash

declare -ar choices=('foo' 'bar' 'baz')

string="*@($(IFS="|";echo "${choices[*]}"))*"    # value contains one option
#string="@($(IFS="|";echo "${choices[*]}"))"     # value is exactly one option

shopt -s extglob
function do_work {
  case "$1" in 
    $string)   echo 'yes!' ;;
    *)         echo 'no!'  ;;
  esac
}

complete -W "${choices[*]}" do_work

但这正在改变 extglob 的选项,我们可以通过将其返回到退出前启动脚本时具有的任何值来避免这种情况。请注意, 的值$string是在函数定义时扩展的,而不是在函数执行时扩展的。但一个更简单的解决方案是避免设置(并且可能通过使用测试=内部的右侧[[...]]工作这一事实来取消设置该 shell 选项就像启用了 extglob shell 选项一样(不,抱歉,这在 中不起作用[...])。因此,我们可以将 case 表达式简化为[[ $1 = $string ]]and (对于 echo 命令),这有效:

[[ $1 = $string ]] && echo 'yes!' || echo 'no!'

但是,一般来说,最好使用函数,并且由于函数不能保证返回退出代码0,我们实际上应该使用:

if [[ $1 = $string ]]; then yesaction; else noaction; fi

由于我们已经解决了这个答案的难点,我们也可以使完成也能够使用带空格的选项。所需要的只是 IFS 拆分选项时将${complete_var}每个选项放在引号内并用空格分隔。

#!/bin/bash

declare -a choices=('foo' 'foo bar' 'baz')

option_string="@($(printf -v var "%s|"       "${choices[@]}"; printf '%s' "${var%?}" ))"
complete_var="$(   printf -v var "'\"%s\"' " "${choices[@]}"; printf '%s' "${var%?}"  )"

#printf '%s\n' "${option_string}" "${complete_var}"

yesaction() { echo 'yes!'; }
noaction () { echo 'no!' ; }

do_work () {
    if    [[ $1 == $option_string ]]
    then  yesaction
    else  noaction
    fi    
}

complete -W "${complete_var}" do_work

return 34;
exit

这会将这些值设置为函数内的固定值。无需设置choices为只读。并允许选项内有空格。

这是假设您将.在 shell 中获取 () 脚本。就像是:

$ . ./script.sh
$ do_work <kbd>TAB</kbd>-<kbd>TAB</kbd>      # will show "foo" "bar" and "foo bar" as options.

我能看到的唯一问题是,完成的值将显示在双引号内:"foo"例如。但在我看来,这不是一个需要解决的问题。

答案3

您还可以使用=~运算符来检查值是否在数组中。

if [[ " ${choices[*]} " =~ " ${1} " ]]; then
    echo "yes!";
else
    echo "no!";
fi

相关内容