POSIX sh相当于Bash的printf%q

Jes*_*ick 18 bash quotes printf posix sh

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

如果我没有对参数进行处理,othercmd "$@"那么工作正常,但我需要提取一些参数并稍微处理它们.

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

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

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

#!/bin/sh
args=
for arg in "$@"
do
    args="'$arg' $args"
done
eval "ls $args"
Run Code Online (Sandbox Code Playgroud)

适用于许多情况:

$ ./handle goodbye "cruel world"
ls: cannot access cruel world: No such file or directory
ls: cannot access goodbye: No such file or directory
Run Code Online (Sandbox Code Playgroud)

但不是在'使用时:

$ ./handle goodbye "cruel'st world"
./handle: 1: eval: Syntax error: Unterminated quoted string
Run Code Online (Sandbox Code Playgroud)

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

#!/bin/bash
args=
for arg in "$@"
do
    printf -v argq '%q' "$arg"
    args="$argq $args"
done
eval "ls $args"
Run Code Online (Sandbox Code Playgroud)

mtr*_*eur 8

这是绝对可行的。

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

首先,您可能已经知道这一点,echo 是一个坏主意,如果目标是可移植性,则应该使用 printf echo 的实现将 -n 视为一个特殊选项,而其他实现则将其视为打印的普通参数。所以就变成了这样:

esceval()
{
    printf %s "$1" | sed "s/'/'\"'\"'/g"
}
Run Code Online (Sandbox Code Playgroud)

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

'"'"'
Run Code Online (Sandbox Code Playgroud)

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

'\''
Run Code Online (Sandbox Code Playgroud)

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

esceval()
{
    printf %s "$1" | sed "s/'/'\\\\''/g"
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

这将确保参数已经完全转义,在构建最终字符串时无需添加更多单引号。这可能是您将获得的最接近单个、可内联版本的东西。如果您对 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 \'
}
Run Code Online (Sandbox Code Playgroud)

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

printf %s "${UNESCAPED%%\'*}""'\''"
Run Code Online (Sandbox Code Playgroud)

..这可以替换为:

printf %s "${UNESCAPED%%\'*}'\''"
Run Code Online (Sandbox Code Playgroud)

我做前者的唯一原因是因为从前有 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 \'
  )
}
Run Code Online (Sandbox Code Playgroud)

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

不要害怕,我已经为您提供了:

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"
}
Run Code Online (Sandbox Code Playgroud)

.. 或同样的事情,但只有 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"
  )
}
Run Code Online (Sandbox Code Playgroud)

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

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


orm*_*aaj 1

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