23

假设我有一个#!/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"
4

5 回答 5

11

这是绝对可行的。

您看到的 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,它不会能够执行下一个变体 - 但您需要关心的几乎所有外壳都可以做到这一点):

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 在将变量替换为带引号的字符串时存在错误,其中变量周围的引号并没有完全从变量替换的开始和结束位置开始。因此,这是我偏执的便携习惯。在实践中,你可以做后者,这不会有问题。

如果您不想unescaped在 shell 环境的其余部分中破坏变量,则可以将该函数的全部内容包装在子 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 - 我将它们分开,因为我觉得当您可以看到单独的开始和结束单引号时它使逻辑更加清晰打印报表。

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

于 2015-04-23T13:08:33.327 回答
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。

于 2012-08-28T15:28:55.137 回答
1

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

#!/bin/sh
quote() {
    echo "$1" | sed "s/'/'\"'\"'/g"
}
args=
for arg in "$@"
do
    argq="'"`quote "$arg"`"'"
    args="$argq $args"
done
eval "ls $args"
于 2012-08-28T15:35:14.930 回答
1

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

于 2012-08-28T15:08:38.033 回答
0

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

esceval() {
    /usr/bin/printf '%q ' "$@"
}
于 2021-06-19T00:36:44.607 回答