POSIX sh相当于Bash的printf%q

时间:2012-08-28 14:54:09

标签: bash posix printf quotes sh

假设我有一个#!/bin/sh脚本,它可以采用各种位置参数,其中一些可能包含空格,两种引号/两种引号等。我想迭代"$@"并且每个参数要么以某种方式立即处理,要么保存以供日后使用。在脚本结束时,我想要启动(也许是exec)另一个进程,传递一些这些参数,并保留所有特殊字符。

如果我没有对参数进行处理,othercmd "$@"可以正常工作,但我需要提取一些参数并稍微处理一下。

如果我可以假设Bash,那么我可以使用printf %q来计算我之后可以eval引用的args版本,但是这不会对例如Ubuntu的短跑(/bin/sh)。

是否可以使用内置函数和POSIX定义的实用程序在简单的Bourne shell脚本中编写printf %q的等价物,比如我可以复制到脚本中的函数吗?

例如,脚本以相反的顺序尝试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"

5 个答案:

答案 0 :(得分:3)

这绝对可行。

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中)尾随新行(不是所有空格,只是具体换行符)。所以上述解决方案是有效的,除非你在参数的最后有新行。然后你会丢失/那些换行符。修复显然很简单:在从quote / 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"
}

..或同样的事情,但只有shell版本:

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 - 我将它们分开,因为我觉得当你能看到起始和结束单个时它使逻辑更清晰 - 在单独的印刷声明中引用。

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

答案 1 :(得分: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的推。

答案 2 :(得分:1)

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

答案 3 :(得分:1)

以下似乎适用于我迄今为止所做的一切,包括空格,两种引号和各种其他元字符以及嵌入的换行符:

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

答案 4 :(得分:0)

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

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