繁体   English   中英

POSIX sh 等效于 Bash 的 printf %q

[英]POSIX sh equivalent for Bash’s printf %q

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

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

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

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

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

#!/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"

这绝对可行。

你在Jesse Glick看到的答案就在那里,但它有一些错误,我还有一些替代方案供你考虑,因为这是我遇到的问题不止一次。

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

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

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

'"'"'

..而你可以把它们变成:

'\''

我猜想(我认为性能差异可以忽略不计,尽管我从未测试过)。 生成的sed字符串如下所示:

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

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

但是,我们仍然有一个错误:命令替换将从命令输出中删除至少一个(但在许多shell ALL中)尾随换行符(并非所有空格,特别是换行符)。 所以上述解决方案是有效的,除非你在参数的最后有新行。 然后你会丢失/那些换行符。 修复显然很简单:在从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 - 我将它们分开,因为我觉得当你可以在单独的单引号上看到它时逻辑更清晰打印报表。

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

我认为这是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" "$@"set -- "$@" "$arg"

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

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

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

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

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

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

当 GNU Coreutil 版本不低于 8.25 时,我们可以使用 /usr/bin/printf

#!/bin/sh

minversion="8.25"
gnuversion=$(ls '--version' | sed '1q' | awk 'NF{print $NF}')

printcmd="printf"

if ! [ $gnuversion \< $minversion ]; then
    printcmd="/usr/bin/printf"
fi;

params=$($printcmd "%q" "$@")

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM