转义变量以用作另一个脚本的内容

Wal*_*alf 38 bash variable-substitution variable bash-expansion

这个问题不是关于如何编写正确转义​​的字符串文字。我找不到任何与如何转义变量以在脚本或其他程序中直接使用的相关问题。

我的目标是使脚本能够生成其他脚本。这是因为生成的脚本中的任务将在另一台机器上运行 0 到n次,并且生成它们的数据在运行(再次)之前可能会发生变化,因此直接通过网络执行操作将不行。

给定一个可能包含特殊字符(例如单引号)的已知变量,我需要将其写为完全转义的字符串文字,例如,foo包含的变量bar'baz应出现在生成的脚本中:

qux='bar'\''baz'
Run Code Online (Sandbox Code Playgroud)

这将通过附加"qux=$foo_esc"到其他脚本行来编写。我是这样使用 Perl 做到的:

foo_esc="'`perl -pe 's/('\'')/\\1\\\\\\1\\1/g' <<<"$foo"`'"
Run Code Online (Sandbox Code Playgroud)

但这似乎有点矫枉过正。

我单独使用 bash 没有成功。我已经尝试了这些的许多变体:

foo_esc="'${file//\'/\'\\\'\'}'"
foo_esc="'${file//\'/'\\''}'"
Run Code Online (Sandbox Code Playgroud)

但是输出中会出现额外的斜杠(当我这样做时echo "$foo"),或者它们会导致语法错误(如果从 shell 完成,则需要进一步的输入)。

Mic*_*mer 48

Bash 对于这种情况有一个参数扩展选项

${parameter@Q}扩展是一个字符串,它是引用格式的参数值,可以作为输入重用。

所以在这种情况下:

foo_esc="${foo@Q}"
Run Code Online (Sandbox Code Playgroud)

这在 Bash 4.4 及更高版本中受支持。对于其他形式的扩展,以及专门生成完整的赋值语句 ( @A) ,也有几个选项。

  • 整洁,但只有 4.2,它给出了“糟糕的替代”。 (10认同)
  • Z shell 等价于 `"${foo:q}"`。 (8认同)
  • 我找到了答案:“${(@qq)foo}” (3认同)
  • @JdeBP Z shell 等效项不起作用。zsh还有其他想法吗? (2认同)
  • 请注意,`${var@Q}` 实际上是从 mksh 复制的,并不是最安全的使用方法。详情见[我的回答](/a/600214)。 (2认同)

Dej*_*ton 20

Bash 提供了一个printf带有%q格式说明符的内置函数,即使在较旧的 (<4.0) 版本的 Bash 中,它也会为您执行 shell 转义:

printf '[%q]\n' "Ne'er do well"
# Prints [Ne\'er\ do\ well]

printf '[%q]\n' 'Sneaky injection $( whoami ) `ls /root`'
# Prints [Sneaky\ injection\ \$\(\ whoami\ \)\ \`ls\ /root\`]
Run Code Online (Sandbox Code Playgroud)

此技巧还可用于从函数返回数据数组:

function getData()
{
  printf '%q ' "He'll say hi" 'or `whoami`' 'and then $( byebye )'
}

declare -a DATA="( $( getData ) )"
printf 'DATA: [%q]\n' "${DATA[@]}"
# Prints:
# DATA: [He\'ll\ say\ hi]
# DATA: [or\ \`whoami\`]
# DATA: [and\ then\ \$\(\ byebye\ \)]
Run Code Online (Sandbox Code Playgroud)

请注意,Bashprintf内置printf程序与大多数类 Unix 操作系统捆绑的实用程序不同。如果出于某种原因,该printf命令调用实用程序而不是内置命令,则您始终可以执行builtin printf


Sté*_*las 14

TL;DR:跳到结论。

虽然有几个外壳/工具内置了引用运算符,其中一些已经在一些答案中提到过,但我想在这里强调,许多使用不安全,具体取决于:

  • 引用的是什么
  • 使用带引号的字符串的上下文。
  • 生成引用输出的语言环境
  • 稍后使用生成的引用输出的语言环境。

需要考虑的几件事:

  • 在某些情况下,将空字符串表示为''or很重要""。例如,如果要在其中使用sh -c "cmd $quoted_output"它,那么我们是否希望将引用的内容作为一个参数传递给cmd. 在 中sh -c "var=$quoted_output; ...",空字符串是表示为''""还是表示为空字符串都没有关系。

    $var:q运算符zsh将空字符串表示为空字符串,not ''""nor $''

    ${var@Q}运算符bash(本身从其复制而来mksh,在这方面的行为有所不同),表示一个空的$varas '',但未设置$var为空字符串:

    $ empty_var= bash -c 'printf "<%s>\n" "${empty_var@Q}" "${unset_var@Q}"'
    <''>
    <>
    $ empty_var= mksh -c 'printf "<%s>\n" "${empty_var@Q}" "${unset_var@Q}"'
    <''>
    <''>
    $ empty_var= zsh -c 'printf "<%s>\n" "${empty_var:q}" "${unset_var:q}"'
    <>
    <>
    
    Run Code Online (Sandbox Code Playgroud)
  • 其中一些运营商报价将使用的组合'...'\"..."$'...'。后者的语法在 shell 和给定 shell 的版本之间有所不同。因此,对于那些确实使用它或可以根据输入使用它的运算符,重要的是在同一个 shell(及其相同版本)中使用结果。这至少适用于:

    • printf %qGNU的printfbashksh93zsh
    • zsh$var:q${(q)var}${(q+)var}${(qqqq)var}
    • mksh${var@Q}
    • bash${var@Q}
    • typeset/ declare/export -p的输出ksh93mkshzsh
    • , , ,的alias/set输出bashksh93mkshzsh
    • , ,的xtrace输出ksh93mkshzsh

    在任何情况下$'...'都不是(还¹)标准的sh引用运算符,请注意非 Bourne-like shell,例如rc, es, akangafish具有完全不同的引用语法。根本无法以与存在的每个 shell 兼容的方式引用字符串(尽管请参阅其他问答以了解一些解决方法)。

  • 有些shell在解释其中的代码之前将它们的输入解码为字符,有些不这样做,有些有时这样做,有时不这样做。

    一些 shell(如bash)也使它们的语法以语言环境为条件。例如,语法中的标记定界符是在yash和的区域设置中被视为空白的字符bash(尽管 inbash仅适用于单字节字符)。一些 shell 还依赖于语言环境的字符分类来决定变量名中哪些字符是有效的。因此,例如Stéphane=1可以解释为一种语言环境中的赋值,或Stéphane=1另一种语言环境中命令的调用。

    字节序列 0xa3 0x5c 表示£\ISO-8859-1(又名 latin1)字符集中的?字符串、BIG5 中的字符或 UTF-8 中的无效字节序列。\恰好是 shell 语法中的一个特殊字符,包括 inside"..."$'...'`也是一个(危险的)字符,其编码可以在某些语言环境的其他字符的编码中找到。

    字节0xa0是大量单字节字符集中的不间断空格字符,该字符在某些系统的某些语言环境中被视为空白,例如bash或语法中的标记分隔符yash

    该字节也可以在数千个字符的 UTF-8 编码中找到,其中包括许多字母字符(例如à,编码为 0xc3 0xa0)。

    我不知道在任何基于 ASCII 的系统的任何语言环境中使用任何字符集,这些系统的字符的编码包含'虽然编码。

    有些shell引用运营商输出$'\u00e9'$'\u[e9]'用于é例如字符。反过来,在使用时,取决于 shell,以及在解释或运行使用它的代码时的语言环境,将扩展为其 UTF-8 编码或语言环境的编码(如果语言环境不同,则行为会发生变化)没有那个性格)。

    因此,不仅在相同的 shell 和 shell 版本中使用结果字符串很重要,而且在相同的语言环境中使用它也很重要(至少对于那些进行某些字符编码/解码的 shell)。即便如此,几个 shell(包括bash)在这方面已经或已经有错误。

    任何使用$'...'"..."或反斜杠进行引用或未引用某些非 ASCII 字符的引用运算符都可能不安全。

    或者换句话说,只有'...'在这方面使用的才是安全的。那留下:

    • zsh${(qq)var}操作员
    • /的alias输出,(至少当前版本)。dashbashbosh
    • 所述export -pdash/ bosh(至少当前版本)。
    • (至少当前版本)的set输出dash

    尽管只有第一个被记录并承诺始终使用单引号(但请注意rcquotes下面的警告)。

    另请注意,yash无法处理无法在语言环境字符集中解码的数据,因此无法将任意数据传递给该 shell(至少在当前版本中)。

    具有讽刺意味的是,该locale实用程序的输出存在问题(因为需要使用它"..."来输出隐含设置),并且它通常旨在用于在不同于locale调用位置(以恢复区域设置)的区域设置中输入代码。

  • NUL 字符(0 字节)不能出现在环境变量或通过execve()系统调用执行的命令的参数中(这是该系统调用的限制,它将这些 env 和参数字符串作为 C 风格的 NUL 分隔字符串)。除了在 中zsh,在 shell 变量或内置参数或更一般的 shell 代码中也找不到 NUL。

    然而,0 字节可以从/向文件或管道或任何 I/O 机制读取写入

    zsh它可以被存储在一个变量,读取和写入,作为参数在任何现代编程语言传递给内建像(如pythonperl)。

    但是记住,如果你引用一个NUL任何方法叶子它就好了(相对于$'\0'$'\x0'$'\u0000'$'\C@'例如),不管它是如何引用,结果不能在争论或的环境变量到传递执行命令,其他 shell 将无法使用该 NUL 字符。

    如果您zshIFS= read -r var. 如果 NUL 字节包含在从 stdin 读取的那一行中,$var并且${(qq)var}将包含它,这可能会限制您可以使用它做什么。

    在这种情况下,最好使用$'...'引用形式(如果可以解决与该引用形式相关的其他注意事项(见上文))。

  • 如果生成的引用文本将用于位于反引号内的 shell 代码,请注意有一个额外的反斜杠解释层。始终$(...)代替`...`.

  • 某些字符仅在某些上下文中是特殊的。例如=,在命令名称之前的单词中是特殊的(如 in a=1 cmd arg),但不是之后²(如 in cmd a=1),尽管在某些 shell 中存在一些特殊情况,例如export, readonly...

    ~ 在某些情况下是特殊的,而在其他情况下则不是。

    并非所有引用运算符都会引用这些。

    有些字符在某些 shell 中是特殊的,但在其他 shell 中是特殊的,或者仅在启用某些选项时......

    在某些情况下,甚至数字也是特殊的。例如sh -c "echo ${quoted_text}>file"不会在 中输出引用的文本file,如果2没有被引用'2',例如。

  • 在 中zsh,该rcquotes选项会影响单引号字符串的解释方式(并由其引用运算符生成)。启用后,单引号可以用单引号字符串表示,''rcshell 中使用like 。例如,"foo'bar"也可以写成'foo''bar'

    因此,重要的rcquotes是启用时生成的带引号的字符串只能由zshrcquotes启用的实例解释。

    一个${(qq)var}由使用或不使用的zsh产品rcquotes应该是安全的使用zsh -o rcquotes,但注意到zsh -o rcquotes,串联单引号的字符串会导致一个单引号插入它们之间。

    $ quoted_text="'*'"
    $ zsh -o rcquotes -c "echo $quoted_text$quoted_text"
    *'*
    
    Run Code Online (Sandbox Code Playgroud)

    与...一样:

    $ rc -c "echo $quoted_text$quoted_text"
    *'*
    
    Run Code Online (Sandbox Code Playgroud)

    您可以通过""在两者之间插入来解决它:

    $ zsh -o rcquotes -c "echo $quoted_text\"\"$quoted_text"
    **
    
    Run Code Online (Sandbox Code Playgroud)

    虽然 inrc和导数( where"..."不是引用运算符,'...'是唯一一种引号,因此需要能够'在其中插入),您可以使用^

    $ rc -c "echo $quoted_text^$quoted_text"
    **
    
    Run Code Online (Sandbox Code Playgroud)

综上所述

唯一安全的引用方法(如果我们限制为类似 Bourne 的 shell 并忽略yash和/`...`或流氓语言环境,并假设数据不包含 NUL 字符)是所有内容的单引号(即使是空字符串,甚至是你想要的字符)想象永远不会成为问题),并将单引号字符本身表示为单引号\'"'"在单引号之外,正如您的问题的最初意图。

为此,您可以使用:

  • zsh${(qq)var}运算符(或"${(qq@)array}"用于数组),假设rcquotes未启用该选项。

  • 一个功能,如:

    shquote() {
      LC_ALL=C awk -v q="'" '
        BEGIN{
          for (i=1; i<ARGC; i++) {
            gsub(q, q "\\" q q, ARGV[i])
            printf "%s ", q ARGV[i] q
          }
          print ""
        }' "$@"
    }
    
    Run Code Online (Sandbox Code Playgroud)

    或者

    shquote() {
      perl -le "print join ' ', map {q(') . s/'/'\\\\''/gr . q(')} @ARGV" -- "$@"
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • ksh93/ zsh/ bash/ mksh

    quoted_text=\'${1//\'/\'\\\'\'}\'
    
    Run Code Online (Sandbox Code Playgroud)

    (不要双引号扩展并且不要在标量变量赋值之外使用它,否则您将遇到不同版本之间的兼容性问题bash(请参阅compat41选项说明))


¹ POSIX 规范$'...'最初是针对 Single UNIX 规范第 8 期的,预计最早在 2021 年发布,但看起来它不会成功(没有及时就解决方案达成共识)。因此,我们可能还得至少再等十年才能$'...'将其添加到标准中

² 除非启用了 Bourne shell的-k( keyword) 选项及其某些派生类


Wal*_*alf 9

我想我没有 RTFM。可以这样做:

q_mid=\'\\\'\'
foo_esc="'${foo//\'/$q_mid}'"
Run Code Online (Sandbox Code Playgroud)

然后echo "$foo_esc"给出预期'bar'\''baz'


我如何实际使用它是一个函数:

function esc_var {
    local mid_q=\'\\\'\'
    printf '%s' "'${1//\'/$mid_q}'"
}

...

foo_esc="`esc_var "$foo"`"
Run Code Online (Sandbox Code Playgroud)

修改它以使用printfDejay 解决方案中的内置:

function esc_vars {
    printf ' %q' "$@" | cut -b 2-
}
Run Code Online (Sandbox Code Playgroud)

为了注意Stéphane关于不同版本 bash 之间不兼容的警告,关于双引号扩展中的单引号,防弹函数变为:

esc_vars() {
    local fmt
    fmt='%s'
    local v
    while [ $# -gt 0 ]; do
        v=\'${1//\'/\'\\\'\'}\'
        printf "$fmt" "$v"
        fmt=' %s'
        shift
    done
}
Run Code Online (Sandbox Code Playgroud)


ImH*_*ere 6

引用 var 值有几种解决方案:

  1. 别名
    在大多数 shell 中(在别名可用的情况下)(除了 csh、tcsh 和其他可能的 csh 之类的):

    $ alias qux=bar\'baz
    $ alias qux
    qux='bar'\''baz'
    
    Run Code Online (Sandbox Code Playgroud)

    是的,这适用于许多sh类似 shell 的 shell,例如 dash 或 ash。

  2. set
    同样在大多数 shell 中(同样,不是 csh):

    $ qux=bar\'baz
    $ set | grep '^qux='
    qux='bar'\''baz'
    
    Run Code Online (Sandbox Code Playgroud)

  3. 在某些 shell 中排版(至少是 ksh、bash 和 zsh):

    $ qux=bar\'baz
    $ typeset -p qux
    typeset qux='bar'\''baz'             # this is zsh, quoting style may
                                         # be different for other shells.
    
    Run Code Online (Sandbox Code Playgroud)
  4. 出口
    首先做:

    export qux=bar\'baz
    
    Run Code Online (Sandbox Code Playgroud)

    然后使用:
    export -p | grep 'qux=' export -p | grep 'qux='
    export -p qux

  5. quote
    echo "${qux@Q}"
    echo "${(qq)qux}" # 可以使用一到四个 q。

  • 别名方法很聪明,似乎是由 POSIX 指定的。为了获得最大的便携性,我认为这是要走的路。我相信涉及带有 `export` 或 `set` 的 `grep` 的建议可能会破坏包含嵌入换行符的变量。 (2认同)