在 heredocs 中转义的问题

Problems with escaping in heredocs

我正在编写一个 Jenkins 作业,它将在远程服务器上的两个 chroot 目录之间移动文件。

这使用 Jenkins 多行字符串变量来存储一个或多个文件名,每行一个。

以下内容适用于没有特殊字符或空格的文件:

## Jenkins parameters
# accountAlias = "test" 
# sftpDir = "/path/to/chrooted home"
# srcDir = "/path/to/get/files" 
# destDir = "/path/to/put/files"
# fileName = "file names # multiline Jenkins shell parameter, one file name per

#!/bin/bash
ssh user@server << EOF
#!/bin/bash

printf "\nCopying following file(s) from "${accountAlias}"_old account to "${accountAlias}"_new account:\n"

# Exit if no filename is given so Rsync does not copy all files in src directory. 
if [ -z "${fileName}" ]; then
    
    printf "\n***** At least one filename is required! *****\n"
    exit 1

else

    # While reading each line of fileName
    while IFS= read -r line; do

    printf "\n/"${sftpDir}"/"${accountAlias}"_old/"${srcDir}"/"${line}" -> /"${sftpDir}"/"${accountAlias}"_new/"${destDir}"/"${line}"\n"


    # Rsync the files from old account to new account   
    # -v | verbose
    # -c | replace existing files based on checksum, not timestamp or size
    # -r | recursively copy
    # -t | preserve timestamps
    # -h | human readable file sizes
    # -P | resume incomplete files + show progress bars for large files
    # -s | Sends file names without interpreting special chars
    
    sudo rsync -vcrthPs /"${sftpDir}"/"${accountAlias}"_old/"${srcDir}"/"${line}" /"${sftpDir}"/"${accountAlias}"_new/"${destDir}"/"${line}"

    done <<< "${fileName}"
    
fi

printf "\nEnsuring all new files are owned by the "${accountAlias}"_new account:\n"

sudo chown -vR "${accountAlias}"_new:"${accountAlias}"_new /"${sftpDir}"/"${accountAlias}"_new/"${destDir}"

EOF

使用文件名“sudo bash -c 'echo "hello" > f.txt'.txt”作为测试,我的脚本在文件名中的“sudo”后会失败。

我认为我的问题是我的 $line 变量没有被正确引用或转义,导致 bash 没有将 $line 值视为一个字符串。

我尝试过使用单引号或使用 awk/sed 在变量字符串中插入反斜杠,但这没有用。

我的理论是我 运行 遇到了特殊字符和 heredoc 的问题。

尽管从您的描述中我不清楚您遇到了什么错误或在哪里遇到的错误,但您确实在所提供的脚本中存在一些问题。

主要的可能只是您尝试在远程端执行的 sudo 命令。除非 user 具有无密码 sudo 特权(相当危险),sudo 将提示输入密码并尝试从用户终端读取密码。您没有提供密码。如果实际上您收集了它,您可能只是将它插入命令流(在此处的文档中)。尽管如此,这仍然存在一个潜在的问题,因为您可能会执行许多 sudo 命令,并且它们可能会或可能不会请求密码,具体取决于远程 sudo 配置和 sudo 命令之间的时间间隔。最好是构建命令流,以便只需要执行一次 sudo

其他注意事项如下。


## Jenkins parameters
# accountAlias = "test" 
# sftpDir = "/path/to/chrooted home"
# srcDir = "/path/to/get/files" 
# destDir = "/path/to/put/files"
# fileName = "file names # multiline Jenkins shell parameter, one file name per

#!/bin/bash

#!/bin/bash 没有脚本的第一行,所以它不能作为 shebang 行。相反,它只是一个普通的评论。结果,直接执行脚本时,运行可能是bash,也可能不是bash,如果是bash,则可能是[=,也可能不是[=] 112=]宁在 POSIX 兼容模式下。

ssh user@server << EOF
#!/bin/bash

#!/bin/bash 也不是 shebang 行,因为它仅适用于从常规文件读取的脚本。因此,以下命令是 运行 user 的默认 shell,无论它是什么。如果你想确保剩下的是 运行 by bash,那么也许你应该明确地执行 bash


printf "\nCopying following file(s) from "${accountAlias}"_old account to "${accountAlias}"_new account:\n"

$accountAlias 的两次扩展(由本地 shell)导致 未引用 文本传递给远程 printf 105=]。您可以考虑只删除 de-quoting,但这仍然会使您容易受到包含 double-quote 字符的恶意 accountAlias 值的影响。请记住,在通过网络发送命令之前,这些将在本地进行扩展,然后数据将由远程 shell 处理,也就是解释引用的那个。

这可以通过

解决
  1. 在 heredoc 之外,准备一个可以安全地呈现给远程的帐户别名版本 shell

    accountAlias_safe=$(printf %q "$accountAlias")
    

  2. 在 heredoc 中,展开未引用的内容。我还建议将它作为一个单独的参数传递,而不是将它插入到更大的字符串中。

    printf "\nCopying following file(s) from %s_old account to %s_new account:\n" ${accountAlias_safe} ${accountAlias_safe}
    

类似地适用于大多数其他地方,其中来自本地 shell 的变量被插入到 heredoc 中。


这里...

# Exit if no filename is given so Rsync does not copy all files in src directory. 
if [ -z "${fileName}" ]; then

...为什么要在远程端执行此测试?您可以通过在本地执行它来省去一些麻烦。


这里...

    printf "\n/"${sftpDir}"/"${accountAlias}"_old/"${srcDir}"/"${line}" -> /"${sftpDir}"/"${accountAlias}"_new/"${destDir}"/"${line}"\n"

... 远程 shell 变量 $lineprintf 命令中未加引号使用。它的外观应该被引用。此外,由于您分别使用源名称和目标名称两次,因此将它们放入 (remote-side) 变量中会更清晰明了。并且,如果目录名称具有脚本注释中显示的形式,那么您将引入多余的 / 个字符(尽管这些可能无害)。


对你很好,记录了所有使用的 rsync 选项的含义,但你为什么要通过线路将所有这些发送到远程端?

此外,您可能希望包含 rsync 的 -p 选项以保留相同的权限。可能您还想包含 -l 选项,以复制任何符号 link as symbolic links.


将所有这些放在一起,更像这样的东西(未测试)可能是有序的:

#!/bin/bash

## Jenkins parameters
# accountAlias = "test" 
# sftpDir = "/path/to/chrooted home"
# srcDir = "/path/to/get/files" 
# destDir = "/path/to/put/files"
# fileName = "file names # multiline Jenkins shell parameter, one file name per

# Exit if no filename is given so Rsync does not copy all files in src directory. 
if [ -z "${fileName}" ]; then
    printf "\n***** At least one filename is required! *****\n"
    exit 1
fi

accountAlias_safe=$(printf %q "$accountAlias")
sftpDir_safe=$(printf %q "$sftpDir")
srcDir_safe=$(printf %q "$srcDir")
destDir_safe=$(printf %q "$destDir")
fileName_safe=$(printf %q "$fileName")

IFS= read -r -p 'password for user@server: ' -s -t 60 password || {
    echo 'password not entered in time' 1>&2
    exit 1
}

# Rsync options used:
# -v | verbose
# -c | replace existing files based on checksum, not timestamp or size
# -r | recursively copy
# -t | preserve timestamps
# -h | human readable file sizes
# -P | resume incomplete files + show progress bars for large files
# -s | Sends file names without interpreting special chars
# -p | preserve file permissions
# -l | copy symbolic links as links
    
ssh user@server /bin/bash << EOF
printf "\nCopying following file(s) from %s_old account to %s_new account:\n" ${accountAlias_safe} ${accountAlias_safe}

sudo /bin/bash -c '
while IFS= read -r line; do
    src=${sftpDir_safe}${accountAlias_safe}_old${srcDir_safe}"/${line}"
    dest="${sftpDir_safe}/${accountAlias_safe}_new${destDir_safe}/"${line}"

    printf "\n${src} -> ${dest}\n"

    rsync -vcrthPspl "${src}" "${dest}"
done <<<'${fileName_safe}'

printf "\nEnsuring all new files are owned by the %s_new account:\n" ${accountAlias_safe}

chown -vR ${accountAlias_safe}_new:${accountAlias_safe}_new ${sftpDir_safe}/${accountAlias_safe}_new${destDir_safe}
'
${password}
EOF