【问题标题】:POSIX sh equivalent for Bash’s printf %qPOSIX sh 等效于 Bash 的 printf %q
【发布时间】:2012-08-23 02:57:39
【问题描述】:

假设我有一个#!/bin/sh 脚本,它可以采用各种位置参数,其中一些可能包括空格、两种/两种引号等。我想迭代"$@" 并为每个参数处理它立即以某种方式,或将其保存以备后用。在脚本结束时,我想启动(可能是exec)另一个进程,传入其中一些参数,并且所有特殊字符都完好无损。

如果我不对参数做任何处理,othercmd "$@" 可以正常工作,但我需要拉出一些参数并稍微处理一下。

如果我可以假设 Bash,那么我可以使用 printf %q 来计算引用的 args 版本,我可以稍后使用 eval,但这不适用于例如Ubuntu 的 Dash (/bin/sh)。

是否有任何等效于printf %q 的方法可以用普通的 Bourne shell 脚本编写,只使用内置和 POSIX 定义的实用程序,比如我可以复制到脚本中的函数?

例如,一个脚本试图以相反的顺序 ls 其参数:

#!/bin/sh
args=
for arg in "$@"
do
    args="'$arg' $args"
done
eval "ls $args"

适用于许多情况:

$ ./handle goodbye "cruel world"
ls: cannot access cruel world: No such file or directory
ls: cannot access goodbye: No such file or directory

但在使用' 时不会:

$ ./handle goodbye "cruel'st world"
./handle: 1: eval: Syntax error: Unterminated quoted string

以下工作正常,但依赖于 Bash:

#!/bin/bash
args=
for arg in "$@"
do
    printf -v argq '%q' "$arg"
    args="$argq $args"
done
eval "ls $args"

【问题讨论】:

  • POSIX sh 不是“POSIX Bourne”,而是“POSIX sh”;这是 90 年代早期的规范,它更接近于 ksh88,而不是 70 年代的 Bourne。
  • 我在 libtool 项目的邮件列表中找到了一个用于此 puprose (func_quote) 的可移植且特定于 bash 的函数实现:lists.gnu.org/archive/html/bug-libtool/2015-10/msg00009.html

标签: bash posix printf quotes sh


【解决方案1】:

这是绝对可行的。

您看到的 Jesse Glick 的答案大致就在那里,但它有几个错误,我还有其他一些选择供您考虑,因为这是我不止一次遇到的问题。

首先,您可能已经知道,echo 是一个坏主意,如果目标是可移植性,则应该使用 printf:如果它接收的参数是“-n”,则“echo”在 POSIX 中具有未定义的行为,并且在实践中, echo 的某些实现将 -n 视为特殊选项,而其他实现仅将其视为打印的普通参数。所以就变成了这样:

esceval()
{
    printf %s "$1" | sed "s/'/'\"'\"'/g"
}

或者,不是通过将嵌入的单引号转义为:

'"'"'

..相反,您可以将它们变成:

'\''

..我猜的风格差异(我想无论哪种方式性能差异都可以忽略不计,尽管我从未测试过)。生成的 sed 字符串如下所示:

esceval()
{
    printf %s "$1" | sed "s/'/'\\\\''/g"
}

(这是四个反斜杠,因为双引号会吞下其中两个,留下两个,然后 sed 会吞下一个,只留下一个。就个人而言,我发现这种方式更具可读性,因此我将在其余部分使用涉及它的例子,但两者应该是等价的。)

但是,我们仍然有一个错误:命令替换将从命令输出中删除至少一个(但在许多 shell 中是全部)尾随换行符(不是所有空格,只是换行符)。因此,除非您在参数的最后有换行符,否则上述解决方案有效。然后你会失去那个/那些换行符。修复显然很简单:在实际命令值之后添加另一个字符,然后再从引用/esceval 函数输出。顺便说一句,我们已经需要这样做了,因为我们需要用单引号开始和停止转义参数。你有两种选择:

esceval()
{
    printf '%s\n' "$1" | sed "s/'/'\\\\''/g; 1 s/^/'/; $ s/$/'/"
}

这将确保参数已经完全转义,在构建最终字符串时无需添加更多单引号。这可能是您将获得的最接近单个可内联版本的东西。如果你对 sed 依赖没问题,你可以在这里停下来。

如果您对 sed 依赖项不满意,但可以假设您的 shell 实际上是 POSIX 兼容的(仍然有一些,特别是 Solaris 10 及更低版本上的 /bin/sh,它将无法执行下一个变体 - 但您需要关心的几乎所有 shell 都可以执行此操作):

esceval()
{
    printf \'
    unescaped=$1
    while :
    do
        case $unescaped in
        *\'*)
            printf %s "${unescaped%%\'*}""'\''"
            unescaped=${unescaped#*\'}
            ;;
        *)
            printf %s "$unescaped"
            break
        esac
    done
    printf \'
}

您可能会注意到这里看似多余的引用:

printf %s "${unescaped%%\'*}""'\''"

..这可以替换为:

printf %s "${unescaped%%\'*}'\''"

我这样做的唯一原因是,曾几何时,Bourne shell 在将变量替换为带引号的字符串时存在错误,其中变量周围的引号并没有完全在变量替换的位置开始和结束。因此,这是我偏执的便携习惯。在实践中,你可以做后者,这不会是一个问题。

如果您不想在 shell 环境的其余部分中破坏变量 unescaped,则可以将该函数的全部内容包装在子 shell 中,如下所示:

esceval()
{
  (
    printf \'
    unescaped=$1
    while :
    do
        case $unescaped in
        *\'*)
            printf %s "${unescaped%%\'*}""'\''"
            unescaped=${unescaped#*\'}
            ;;
        *)
            printf %s "$unescaped"
            break
        esac
    done
    printf \'
  )
}

“但是等等”,你说:“我想在一个命令中对 MULTIPLE 参数执行什么操作?如果我从命令行运行它,我希望作为用户的输出仍然看起来不错且清晰易读不管什么原因。”

别害怕,我有你:

esceval()
{
    case $# in 0) return 0; esac
    while :
    do
        printf "'"
        printf %s "$1" | sed "s/'/'\\\\''/g"
        shift
        case $# in 0) break; esac
        printf "' "
    done
    printf "'\n"
}

..或相同的东西,但只有外壳版本:

esceval()
{
  case $# in 0) return 0; esac
  (
    while :
    do
        printf "'"
        unescaped=$1
        while :
        do
            case $unescaped in
            *\'*)
                printf %s "${unescaped%%\'*}""'\''"
                unescaped=${unescaped#*\'}
                ;;
            *)
                printf %s "$unescaped"
                break
            esac
        done
        shift
        case $# in 0) break; esac
        printf "' "
    done
    printf "'\n"
  )
}

在最后四个中,您可以折叠一些外部 printf 语句并将它们的单引号向上滚动到另一个 printf - 我将它们分开,因为我觉得当您可以看到开始和结束单时它使逻辑更加清晰 -单独打印语句上的引号。

附:我还做了这个怪物,它是一个 polyfill,它将在前两个版本之间进行选择,具体取决于你的 shell 是否能够支持必要的变量替换语法(虽然看起来很糟糕,因为只有 shell 的版本必须是在一个 eval-ed 字符串中,以防止不兼容的 shell 在看到它时吐出):https://github.com/mentalisttraceur/esceval/blob/master/sh/esceval.sh

【讨论】:

  • 好东西,但是在纯 shell 解决方案中您需要 printf "'\\\''" 而不是 printf "'\''"(Github 上的版本,printf "'"'\''"'",完全中断)。要使sed 解决方案具有多行 功能,您需要预先阅读所有 行:esceval(){ printf '%s\n' "$1" | sed -e ':a' -e '$!{N;ba' -e '}' -e "s/'/'\\\\''/g; s/^/'/; s/$/'/"; }。 Quibble:一般为better not to use all-uppercase variable names,以免与环境变量和特殊shell变量发生冲突。
  • 感谢您的周到反馈。重新修改您的sed 解决方案:虽然您和我的都应该是可移植的,但您的更可取,因为它更简单并且不会一次读取所有行。但是,它需要一些调整: (a) 1 ... 替换必须放在一般替换之后,这样后者就不会替换刚刚添加的初始 '; (b) 在我的 sed 解决方案中,\n 必须附加到 printf 命令,以确保准确保留尾随换行符:esceval() { printf '%s\n' "$1" | sed "s/'/'\\\\''/g; 1 s/^/'/; $ s/$/'/"; }
  • 回复printf "'"'\''"'":这对我来说实际上是有道理的,因为它包含'\'',这是尝试在单引号字符串中包含单引号,这不是' t 在 POSIX shell 中支持:"A single-quote cannot occur within single-quotes."
  • Re printf "'\''":我认为这里的问题是 printf 行为的变化,而不是 shell 字符串解析:如果你使用 printf %s "'\''",所有 shell 都应该表现再次相同(在bashdashkshzsh 的最新版本中验证)。 printfbuiltin 大多数 shell,在处理 format string 方面的行为有所不同(顺便提一下,printfutility 形式也因平台而异);通过使用%s,您可以消除这些变化。鉴于此,您甚至可以消除单独的 printf 语句并改用 printf %s "${unescaped%%\'*}'\''"
  • @mklement0 是的,这绝对是 printf 处理格式字符串的不兼容,而不是糟糕的 Bourne shell 语法 - 无论如何,我已经修复了我的帖子中的所有示例,以及我的 github esceval。嘘。同样经过一些思考和测试,我现在明白了为什么 sed-only 方法需要额外的换行符:因为 sed 将换行符解释为文本分隔符,而不是文本文字的一部分。因此,仅当您用一个额外的换行符“填充”它时,在尾随输入换行符之后获取结束引号才有效。我也相应地编辑了我的答案。 (是的,你的“shall”工具很有趣。)
【解决方案2】:

我认为这是 POSIX。它的工作原理是在为 for 循环扩展 $@ 之后清除它,但只清除一次,以便我们可以使用 set 迭代地构建它(反向)。

flag=0
for i in "$@"; do
    [ "$flag" -eq 0 ] && shift $#
    set -- "$i" "$@"
    flag=1
done

echo "$@"   # To see that "$@" has indeed been reversed
ls "$@"

我意识到颠倒参数只是一个示例,但您可以在其他情况下使用 set -- "$arg" "$@"set -- "$@" "$arg" 这个技巧。

是的,我意识到我可能刚刚重新实现了(糟糕的)ormaaj 的 Push。

【讨论】:

  • 很有趣,但可能过于具体到我碰巧选择的反转示例。更典型的是,我想收集多个参数列表、处理选项等。
  • 令我最近感到惊讶的是,(( expr )) 不是 POSIX,尽管它得到了广泛的支持。如果您使用该构造,所有变量都会自动插入(不需要$),这对于更复杂的表达式非常有用。
  • 我浏览规范的速度太快了;我以为我在那里看到了。我将用 POSIX 替换,但任何阅读本文的人都应该随时通过更好的测试来编辑我的答案。
【解决方案3】:

Push。有关示例,请参阅自述文件。

【讨论】:

  • 看起来不错,虽然我希望有足够紧凑的东西,无需许可即可内联。
【解决方案4】:

以下内容似乎适用于我到目前为止所提供的所有内容,包括空格、两种引号和各种其他元字符以及嵌入的换行符:

#!/bin/sh
quote() {
    echo "$1" | sed "s/'/'\"'\"'/g"
}
args=
for arg in "$@"
do
    argq="'"`quote "$arg"`"'"
    args="$argq $args"
done
eval "ls $args"

【讨论】:

  • 这不处理包含换行符的参数。实际上这是非常危险的,因为换行符之后的参数中的文本将作为另一个 shell 命令执行。不要在你的网络服务器上运行它!
  • 如果你给它一个参数'-n',它会在引用的时候变成'',因为echo会把它解析为一个选项。最好将echo "$1" 更改为printf "%s" "$1"。 (破折号,Linux)
  • $ eval echo quote "it's" -bash: unexpected EOF while looking for matching `'' -bash: syntax error: unexpected end of file
【解决方案5】:

如果您可以调用外部可执行文件(如在其他答案中给出的sed 解决方案中),那么您也可以调用/usr/bin/printf。虽然 POSIX shell 内置 printf 确实不支持 %q,但 Coreutils 的 printf 二进制文件确实支持 (since release 8.25)。

esceval() {
    /usr/bin/printf '%q ' "$@"
}

【讨论】:

    猜你喜欢
    • 2014-11-22
    • 2021-09-23
    • 2019-02-27
    • 2019-11-11
    • 2021-12-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多