在 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 处理,也就是解释引用的那个。
这可以通过
解决
在 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
命令中未加引号使用。它的外观应该被引用。此外,由于您分别使用源名称和目标名称两次,因此将它们放入 (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
我正在编写一个 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 处理,也就是解释引用的那个。
这可以通过
解决在 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
命令中未加引号使用。它的外观应该被引用。此外,由于您分别使用源名称和目标名称两次,因此将它们放入 (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