在POSIX sh中替代Bash的[[ ... ]]:脚本兼容性实践

20 阅读2分钟

文章正文markdown格式数据

POSIX sh中替代使用Bash的[[ ... ]]的方法

提问:

我正在使用以下代码来解析传递给脚本的第一个参数。它具有错误处理功能,并且工作方式完全符合我的要求:

if [ -z "$action" ]; then
    printf "[${c_RED}ERROR${c_RESET}] The action must be specified.\n" && exit 1
elif [[ "$action" =~ ^-{0,2}[Hh][Ee][Ll][Pp]$ ]] || [[ "$action" =~ ^-{0,2}[Hh]$ ]]; then
    printf "Usage: pocsag [ACTION] [INPUTMETHOD/REDACTION] [OUTPUTMETHOD/PATHTOFILE/SERVICEACTION]                              "
    printf "Examples:                                                                                                           "
    printf "  pocsag decode rtlsdr cli                                                                                          "
    printf "  pocsag decode netcat file                                                                                         "
    printf "  pocsag redact medical ~/media/signals/pocsag/decoded/POCSAG*                                                      "
    printf "  pocsag service rtlsdr start                                                                                       "
    printf "                                                                                                                    "
    printf "Actions:                                                                                                            "
    printf "  decode                    Envoke the usage of the input tuner, sox and multimon-ng to decode the signals.         "
    printf "  redact                    Copy file but redact regex matching lines of a file. For example: Removing medical TXs. "
    printf "  service                   Used to start/stop the systemd service in user's ~/.config. Relies on rtlsdr_pager_rx   "
    printf "                                                                                                                    "
    printf "Input Methods:                                                                                                      "
    printf "  rtlsdr                    Use an RTLSDR device plugged into the local computer.                                   "
    printf "  netcat                    Listen to localhost:7355 using netcat, then process and output locally.                 "
    printf "                                                                                                                    "
elif ! [[ "$action" =~ ^([Dd][Ee][Cc][Oo][Dd][Ee]|[Rr][Ee][Dd][Aa][Cc][Tt]|[Ss][Ee][Rr][Vv][Ii][Cc][Ee])$ ]]; then
    printf "[${c_RED}ERROR${c_RESET}] The action must be 'decode', 'redact' or 'service'.\n" && exit 3
fi

现在我希望使这个脚本符合POSIX标准,因此不能使用[[ ]]这个Bash特有的语法。我应该如何实现这一点?使用复杂的case语句吗?肯定有更好的方法。

答案 2(得分:7)

exprawk是两个可以进行正则表达式匹配的POSIX工具。expr使用基本正则表达式¹,awk使用扩展正则表达式的变体²。expr存在许多设计缺陷,通常被认为已过时(甚至POSIX也建议不要使用它),因此最好避免使用。 虽然几个shell(至少是zsh和yash)的[(即test)内置函数可以通过=~操作符进行正则表达式匹配,但这是POSIX标准之外的扩展,因此不能在sh脚本中使用。 在这里,您可以定义使用awk进行正则表达式匹配的shell辅助函数match(以及用于不区分大小写变体的imatch):

match() {
  awk -- 'BEGIN{exit(ARGV[1] !~ ARGV[2])}' "$@"
}
imatch() {
  awk -- 'BEGIN{exit(tolower(ARGV[1]) !~ tolower(ARGV[2]))}' "$@"
}

if imatch "$action" '^-{0,2}h(elp)?$'; then...

请注意,严格来说,imatch并不是进行不区分大小写匹配的适当方法,但对于仅ASCII输入和正则表达式来说已经足够好了。 在这里,使用case结构可能同样简单,并且使其更清晰:

case $action in
  ([hH] | -[hH] | --[hH] | [Hh][Ee][Ll][Pp] | -[Hh][Ee][Ll][Pp] | --[Hh][Ee][Ll][Pp]) ...;;
esac

您也可以先删除一个或两个前导的-并转换为小写:

tolower() {
  awk -- 'BEGIN{for (i = 1; i < ARGC; i++) print tolower(ARGV[i])}' "$@"
}
action=${action#-} action=${action#-}
action=$(tolower "$action")

case $action in
  (h | help) ...
esac

这里使用awktolower()进行大小写转换。POSIX工具集中的替代方案包括dd conv=lcasetr '[:upper:]' '[:lower:]',但在GNU工具集中,截至撰写本文时,只有awk版本适用于多字节字符。³ 请注意,[[...]]结构最初来自ksh,而不是bash。它已被包括zsh、bash、yash、busybox ash在内的几个shell复制,并有许多变体。 其中的正则表达式匹配首先于2004年在zsh中添加了-pcre-match操作符(PCRE具有不区分大小写匹配的语法),然后是bash于2005年在3.1版本中添加了=~⁴(进行ERE匹配)。 后来=~被添加到更多shell中,包括zsh和ksh93。在zsh中,对于=~,您可以通过(取消)设置rematchpcre选项来选择ERE或PCRE。 ksh93的模式匹配在2006年的ksh93r+版本中扩展为具有正则表达式匹配语法⁵,因此例如您可以执行[[ $action = ~(Ei)^-{0,2}h(elp)?$ ]](在zmodload zsh/pcre之后,相当于zsh的[[ $action -pcre-match '(?i)^-{0,2}h(elp)$' ]])。

¹ 使用其:操作符。另请注意,expr正则表达式匹配隐式地在开头锚定(就像有一个隐藏的^),而不是在结尾。 ² 除了标准ERE外,还识别像\n/\b/\123...这样的内容,这意味着除了busybox awk之外,您无法获得反向引用(标准ERE中也没有反向引用)。请注意,对区间操作符({x,y})的支持在一些awk实现中相对较晚才添加(就mawk而言是非常近期)。 ³ 不过,已知一些GNU/Linux发行版维护了解决此问题的补丁,因此您的体验可能有所不同。请注意,Ubuntu上的默认awk实现mawk也不支持多字节字符。 ⁴ =~可能源自perl中的相同操作符,而perl又可能源自awk的~操作符。 ⁵ 本质上,其=~操作符在底层执行一个前面附加了~(E)的模式=