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

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

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

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

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

qux='bar'\''baz'

这将通过附加"qux=$foo_esc"到脚本的其他行来编写。我用 Perl 做到了,如下所示:

foo_esc="'`perl -pe 's/('\'')/\\1\\\\\\1\\1/g' <<<"$foo"`'"

但这似乎有点矫枉过正。

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

foo_esc="'${file//\'/\'\\\'\'}'"
foo_esc="'${file//\'/'\\''}'"

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

答案1

Bash 有一个参数扩展选项正好适合这种情况:

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

所以在这种情况下:

foo_esc="${foo@Q}"

Bash 4.4 及更高版本支持此功能。对于其他形式的扩展,以及专门生成完整的赋值语句(),还有多种选项@A

答案2

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

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\`]

这个技巧也可以用于从函数返回数据数组:

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\ \)]

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

答案3

TL;DR:跳到结论。

虽然几个 shell/工具都有内置的引用运算符,其中一些已经在一些答案中提到过,但我想在这里强调一下许多使用起来不安全根据:

  • 所引用的内容
  • 使用带引号的字符串的上下文。
  • 生成带引号的输出的区域设置
  • 稍后使用生成的带引号输出的区域设置。

需要考虑的几件事:

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

    运算$var:q符 ofzsh将空字符串表示为空字符串, 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}"'
    <>
    <>
    
  • '...'其中一些引用运算符将​​使用, \,"..."或的组合$'...'。后者的语法在 shell 之间以及给定 shell 的版本之间有所不同。因此,对于那些确实使用它或可以根据输入使用它的运算符来说,重要的是在同一个 shell(及其相同版本)中使用结果。这至少适用于:

    • printf %qGNU的printf, bash, ksh93,zsh
    • zsh$var:q、、、、${(q)var}${(q+)var}${(qqqq)var}
    • mksh${var@Q}
    • bash${var@Q},
    • typeset// declare, , ,export -p的输出(不适用于旧版本中的标量变量)。ksh93mkshzshbash
    • alias/set的输出bash, ksh93, mksh,zsh
    • xtrace的输出ksh93, mksh,zsh

    无论如何,$'...'它(还不是)一个标准的sh引用运算符,并且要注意非类 Bourne shell,例如rc, es, akanga,fish已经完全不同的引用语法。根本没有办法以与现有的每个 shell 兼容的方式引用字符串(尽管请参阅这是另一个问答一些解决方法)。

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

    一些 shell(例如bash)还使其语法以语言环境为条件。例如,语法中的标记分隔符是在yash和中的语言环境中被视为空格的字符bash(尽管在 中bash,这只适用于单字节字符)。某些 shell 还依赖于语言环境的字符分类来决定变量名称中哪些字符有效。例如,Stéphane=1可以解释为一种语言环境中的赋值,或解释为Stéphane=1另一种语言环境中命令的调用。

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

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

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

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

    例如,某些 shell 引用运算符输出$'\u00e9'$'\u[e9]'字符。é反过来,在使用时,根据 shell 以及解释或运行使用它的代码时的语言环境,将扩展到其 UTF-8 编码或语言环境的编码(如果语言环境不同,则行为会有所不同)没有这个字符)。

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

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

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

    • zsh${(qq)var}运算符
    • /alias的输出(至少当前版本)。dashbashbosh
    • export -p/ dashbosh至少当前版本)。
    • (至少当前版本)set的输出。dash

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

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

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

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

    然而 0 字节可以是书面可以从/到文件或管道或任何 I/O 机制。

    zsh可以存储在变量中,进行读取和写入,作为参数传递给内置函数,就像任何现代编程语言(例如pythonperl)一样。

    但请记住,如果您使用任何按原样保留 NUL 的方法(而不是$'\0', $'\x0', $'\u0000',$'\C@'例如)引用 NUL,则无论如何引用它,结果都不能在参数或环境变量中传递给被处决命令,并且任何其他 shell 都无法使用该 NUL 字符。

    zsh如果您在 中接受外部输入(如在 中),则可能需要记住这一点IFS= read -r var。如果从 stdin 读取的该行中包含 NUL 字节,$var并且${(qq)var}将包含它,这可能会限制您可以使用它执行的操作。

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

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

  • 有些字符仅在某些上下文中才特殊。例如,=在命令名称之前的单词是特殊的(如a=1 cmd arg),但在命令名称之后的单词不是特殊的(如),尽管在某些 shell 中对于诸如, ...cmd a=1之类的命令有一些特殊情况。exportreadonly

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

    并非所有引用运算符都会引用它们。

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

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

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

    因此,重要的是,rcquotes启用时生成的引用字符串只能由zsh也已启用的实例解释rcquotes

    ${(qq)var}带有或不带有 zsh 的生成应该rcquotes可以安全地在 中使用zsh -o rcquotes,但请注意,在 中zsh -o rcquotes,连接单引号字符串将导致在它们之间插入单引号。

    $ quoted_text="'*'"
    $ zsh -o rcquotes -c "echo $quoted_text$quoted_text"
    *'*
    

    与...一样:

    $ rc -c "echo $quoted_text$quoted_text"
    *'*
    

    ""您可以通过在两者之间插入来解决这个问题:

    $ zsh -o rcquotes -c "echo $quoted_text\"\"$quoted_text"
    **
    

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

    $ rc -c "echo $quoted_text^$quoted_text"
    **
    

综上所述

唯一安全的引用方法(如果我们限制为类似 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 ""
        }' "$@"
    }
    

    或者

    shquote() {
      perl -le "print join ' ', map {q(') . s/'/'\\\\''/gr . q(')} @ARGV" -- "$@"
    }
    
  • ksh93/// :zsh​​bashmksh

    quoted_text=\'${1//\'/\'\\\'\'}\'
    

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


^POSIX 规范$'...'最初的目标是单一 UNIX 规范第 8 期,预计最早将于 2021 年发布,但看起来不会实现(尚未及时就解决方案达成共识)。因此,我们可能还需要至少再等十年才能将$'...'其添加到标准中

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

答案4

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

  1. 别名
    在大多数 shell 中(其中别名可用)(除了 csh、tcsh 以及可能其他类似 csh 的 shell):

    $ alias qux=bar\'baz
    $ alias qux
    qux='bar'\''baz'
    

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

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

    $ qux=bar\'baz
    $ set | grep '^qux='
    qux='bar'\''baz'
    
  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.
    
  4. 导出
    首先做:

    export qux=bar\'baz
    

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

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

相关内容