我正在编写一个 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 在变量字符串中插入反斜杠,但这没有奏效。
我的理论是我遇到了特殊字符和异端文档的问题。
虽然我从您的描述中不清楚您遇到的确切错误或在哪里,但您在所呈现的脚本中确实存在几个问题。
主要命令可能只是您尝试在远程端执行的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
,它可能会也可能不会在 POSIX 兼容模式下运行。
ssh user@server << EOF #!/bin/bash
此#!/bin/bash
也不是 shebang 行,因为这仅适用于从常规文件中读取的脚本。 因此,以下命令由user
的默认 shell 运行,无论它是什么。 如果你想确保其余部分由bash
运行,那么也许你应该显式执行bash
。
printf "nCopying following file(s) from "${accountAlias}"_old account to "${accountAlias}"_new account:n"
$accountAlias
的两次扩展(由本地外壳)导致将不带引号的文本传递给远程外壳中的printf
。 您可以考虑删除取消引号,但这仍然会使您容易受到包含双引号字符的恶意accountAlias
值的影响。 请记住,在通过网络发送命令之前,这些将在本地进行扩展,然后数据将由远程 shell 处理,该 shell 将解释引用。
这可以通过
-
在 heredoc 之外,准备一个可以安全地呈现给远程 shell 的帐户别名版本
accountAlias_safe=$(printf %q "$accountAlias")
和
-
在 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 变量$line
在printf
命令中使用时不加引号。 它的外观应该被引用。 此外,由于源名称和目标名称分别使用两次,因此将它们放在(远程端)变量中会更清晰、更清晰。 并且,如果目录名称具有脚本注释中显示的形式,那么您将引入过多的/
字符(尽管这些字符可能无害)。
对您有好处,记录了所有使用的rsync
选项的含义,但是为什么要通过网络将所有这些发送到远程端?
此外,您可能希望包含 rsync 的-p
选项以保留相同的权限。 您可能还想包含-l
选项,以将任何符号链接复制为符号链接。
把所有这些放在一起,更像这样的东西(未经测试)可能是有序的:
#!/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