这是 bash 脚本的有效自我更新方法吗?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/8595751/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Is this a valid self-update approach for a bash script?
提问by Der Hochstapler
I'm working on a script that has gotten so complex I want to include an easy option to update it to the most recent version. This is my approach:
我正在处理一个变得如此复杂的脚本,我想包含一个简单的选项来将其更新到最新版本。这是我的方法:
set -o errexit
SELF=$(basename runSelfUpdate() {
echo "Performing self-update..."
# Download new version
echo -n "Downloading latest version..."
if ! wget --quiet --output-document=" if ! wget --quiet --output-document=if (( $(wc -c < OCTAL_MODE=$(stat -c '%a' #!/usr/bin/env bash
#
set -fb
readonly THISDIR=$(cd "$(dirname "##代码##")" ; pwd)
readonly MY_NAME=$(basename "##代码##")
readonly FILE_TO_FETCH_URL="https://your_url_to_downloadable_file_here"
readonly EXISTING_SHELL_SCRIPT="${THISDIR}/somescript.sh"
readonly EXECUTABLE_SHELL_SCRIPT="${THISDIR}/.somescript.sh"
function get_remote_file() {
readonly REQUEST_URL=
readonly OUTPUT_FILENAME=
readonly TEMP_FILE="${THISDIR}/tmp.file"
if [ -n "$(which wget)" ]; then
$(wget -O "${TEMP_FILE}" "$REQUEST_URL" 2>&1)
if [[ $? -eq 0 ]]; then
mv "${TEMP_FILE}" "${OUTPUT_FILENAME}"
chmod 755 "${OUTPUT_FILENAME}"
else
return 1
fi
fi
}
function clean_up() {
# clean up code (if required) that has to execute every time here
}
function self_clean_up() {
rm -f "${EXECUTABLE_SHELL_SCRIPT}"
}
function update_self_and_invoke() {
get_remote_file "${FILE_TO_FETCH_URL}" "${EXECUTABLE_SHELL_SCRIPT}"
if [ $? -ne 0 ]; then
cp "${EXISTING_SHELL_SCRIPT}" "${EXECUTABLE_SHELL_SCRIPT}"
fi
exec "${EXECUTABLE_SHELL_SCRIPT}" "$@"
}
function main() {
cp "${EXECUTABLE_SHELL_SCRIPT}" "${EXISTING_SHELL_SCRIPT}"
# your code here
}
if [[ $MY_NAME = \.* ]]; then
# invoke real main program
trap "clean_up; self_clean_up" EXIT
main "$@"
else
# update myself and invoke updated version
trap clean_up EXIT
update_self_and_invoke "$@"
fi
)
case ${OCTAL_MODE:--1} in
-[1] )
printf "Error : OCTAL_MODE was empty\n"
exit 1
;;
777|775|755 ) : nothing ;;
* )
printf "Error in OCTAL_MODEs, found value=${OCTAL_MODE}\n"
exit 1
;;
esac
if ! chmod $OCTAL_MODE ##代码##.tmp ; then
echo "error on chmod $OCTAL_MODE %0.tmp from $UPDATE_BASE/$SELF, can't continue"
exit 1
fi
.tmp) >= $(wc -c < ##代码##) )); then
mv ##代码##.tmp ##代码##
fi
.tmp $UPDATE_BASE/$SELF ; then
echo "error on wget on $UPDATE_BASE/$SELF"
exit 1
fi
.tmp" $UPDATE_BASE/$SELF ; then
echo "Failed: Error while trying to wget new version!"
echo "File requested: $UPDATE_BASE/$SELF"
exit 1
fi
echo "Done."
# Copy over modes from old version
OCTAL_MODE=$(stat -c '%a' $SELF)
if ! chmod $OCTAL_MODE "##代码##.tmp" ; then
echo "Failed: Error while trying to set mode on ##代码##.tmp."
exit 1
fi
# Spawn update script
cat > updateScript.sh << EOF
#!/bin/bash
# Overwrite old file with new
if mv "##代码##.tmp" "##代码##"; then
echo "Done. Update complete."
rm $0
else
echo "Failed!"
fi
EOF
echo -n "Inserting update process..."
exec /bin/bash updateScript.sh
}
)
UPDATE_BASE=http://something
runSelfUpdate() {
echo "Performing self-update..."
# Download new version
wget --quiet --output-document=##代码##.tmp $UPDATE_BASE/$SELF
# Copy over modes from old version
OCTAL_MODE=$(stat -c '%a' ##代码##)
chmod $OCTAL_MODE ##代码##.tmp
# Overwrite old file with new
mv ##代码##.tmp ##代码##
exit 0
}
The script seems to work as intended, but I'm wondering if there might be caveats with this kind of approach. I just have a hard time believing that a script can overwrite itself without any repercussions.
该脚本似乎按预期工作,但我想知道这种方法是否可能存在警告。我只是很难相信脚本可以在没有任何影响的情况下覆盖自己。
To be more clear, I'm wondering, if, maybe, bash would read and execute the script line-by-line and after the mv, the exit 0could be something else from the new script. I think I remember Windows behaving like that with .batfiles.
更清楚地说,我想知道,bash 是否会逐行读取和执行脚本,然后在 . 之后mv,exit 0可能是新脚本中的其他内容。我想我记得 Windows 对.bat文件的行为是这样的。
Update: My original snippet did not include set -o errexit. To my understanding, that should keep me safe from issues caused by wget.
Also, in this case, UPDATE_BASEpoints to a location under version control (to ease concerns).
更新:我的原始片段不包括set -o errexit. 据我了解,这应该可以使我免受wget.
此外,在这种情况下,UPDATE_BASE指向版本控制下的位置(以减轻担忧)。
Result: Based on the input from these answers, I constructed this revised approach:
结果:根据这些答案的输入,我构建了这种修改后的方法:
##代码##采纳答案by Keith Thompson
(At least it doesn't try to continue running after updating itself!)
(至少它不会在自我更新后尝试继续运行!)
The thing that makes me nervous about your approach is that you're overwriting the current script (mv $0.tmp $0) as it's running. There are a number of reasons why this will probablywork, but I wouldn't bet large amounts that it's guaranteed to work in all circumstances. I don't know of anything in POSIX or any other standard that specifies how the shell processes a file that it's executing as a script.
让我对您的方法感到紧张的是,您正在覆盖当前正在运行的脚本 ( mv $0.tmp $0) 。这可能会奏效的原因有很多,但我不会打赌它保证在所有情况下都有效。我不知道 POSIX 或任何其他标准中的任何内容指定 shell 如何处理它作为脚本执行的文件。
Here's what's probably going to happen:
以下是可能会发生的事情:
You execute the script. The kernel sees the #!/bin/shline (you didn't show it, but I presume it's there) and invokes /bin/shwith the name of your script as an argument. The shell then uses fopen(), or perhaps open()to open your script, reads from it, and starts interpreting its contents as shell commands.
你执行脚本。内核看到该#!/bin/sh行(您没有显示它,但我认为它在那里)并/bin/sh以您的脚本名称作为参数进行调用。然后 shell 使用fopen(),或者可能open()打开您的脚本,从中读取,并开始将其内容解释为 shell 命令。
For a sufficiently small script, the shell probably just reads the whole thing into memory, either explicitly or as part of the buffering done by normal file I/O. For a larger script, it might read it in chunks as it's executing. But either way, it probably only opens the file once, and keeps it open as long as it's executing.
对于足够小的脚本,shell 可能只是将整个内容读入内存,或者显式地或者作为正常文件 I/O 完成的缓冲的一部分。对于较大的脚本,它可能会在执行时分块读取。但无论哪种方式,它可能只打开文件一次,并在执行期间一直保持打开状态。
If you remove or rename a file, the actual file is not necessarily immediately erased from disk. If there's another hard link to it, or if some process has it open, the file continues to exist, even though it may no longer be possible for another process to open it under the same name, or at all. The file is not physically deleted until the last link (directory entry) that refers to it has been removed, andno processes have it open. (Even then, its contents won't immediately be erased, but that's going beyond what's relevant here.)
如果您删除或重命名文件,实际文件不一定会立即从磁盘中删除。如果有另一个硬链接,或者如果某个进程打开了它,该文件将继续存在,即使另一个进程可能不再可能以相同的名称打开它,或者根本无法打开它。在删除指向它的最后一个链接(目录条目)之前,不会物理删除该文件,并且没有进程打开它。(即使那样,它的内容也不会立即被删除,但这超出了此处的相关内容。)
And furthermore, the mvcommand that clobbers the script file is immediately followed by exit 0.
此外,mv破坏脚本文件的命令后紧跟exit 0.
BUTit's at least conceivable that the shell could close the file and then re-open it by name. I can't think of any good reason for it to do so, but I know of no absolute guarantee that it won't.
但至少可以想象,shell 可以关闭文件,然后按名称重新打开它。我想不出有什么好的理由这样做,但我知道不能绝对保证它不会。
And some systems tend to do stricter file locking that most Unix systems do. On Windows, for example, I suspect that the mvcommand would fail because a process (the shell) has the file open. Your script might fail on Cygwin. (I haven't tried it.)
并且某些系统倾向于执行大多数 Unix 系统所做的更严格的文件锁定。例如,在 Windows 上,我怀疑该mv命令会失败,因为进程(shell)打开了文件。您的脚本可能会在 Cygwin 上失败。(我没试过。)
So what makes me nervous is not so much the small possibility that it could fail, but the long and tenuous line of reasoning that seems to demonstrate that it will probably succeed, and the very real possibility that there's something else I haven't thought of.
所以让我紧张的不是它可能失败的小可能性,而是似乎证明它可能会成功的冗长而脆弱的推理路线,以及我没有想到的其他事情的真实可能性.
My suggestion: write a second script whose one and only job is to update the first. Put the runSelfUpdate()function, or equivalent code, into that script. In your original script, use execto invoke the update script, so that the original script is no longer running when you update it. If you want to avoid the hassle of maintaining, distributing, and installing two separate scripts. you could have the original script createthe update script with a unique in /tmp; that would also solve the problem of updating the update script. (I wouldn't worry about cleaning up the autogenerated update script in /tmp; that would just reopen the same can of worms.)
我的建议:编写第二个脚本,其唯一的工作就是更新第一个脚本。将runSelfUpdate()函数或等效代码放入该脚本中。在您的原始脚本中,使用exec来调用更新脚本,以便您更新时原始脚本不再运行。如果您想避免维护、分发和安装两个单独脚本的麻烦。您可以让原始脚本创建具有唯一 in 的更新脚本/tmp;这也将解决更新更新脚本的问题。(我不会担心清理 中的自动生成的更新脚本/tmp;那只会重新打开相同的蠕虫罐头。)
回答by shellter
Yes, but ... I would recommend you keep a more layered version of your script's history, unless the remote host can also perform version-control with histories. That being said, to respond directly to the code you have posted, see the following comments ;-)
是的,但是……我建议您保留脚本历史记录的更分层版本,除非远程主机也可以使用历史记录执行版本控制。话虽如此,要直接回复您发布的代码,请参阅以下评论 ;-)
What happens to your system when wget has a hiccup, quietly overwrites part of your working script with only a partial or otherwise corrupt copy? Your next step does a mv $0.tmp $0so you've lost your working version. (I hope you have it in version control on the remote!)
当 wget 出现问题时,您的系统会发生什么情况,仅用部分或其他损坏的副本悄悄覆盖部分工作脚本?您的下一步执行 amv $0.tmp $0因此您丢失了您的工作版本。(我希望你能在遥控器上进行版本控制!)
You can check to see if wget returns any error messages
您可以检查 wget 是否返回任何错误消息
##代码##Also, Rule-of-thumb tests will help, i.e.
此外,经验法则测试会有所帮助,即
##代码##but are hardly foolproof.
但很难做到万无一失。
If your $0 could windup with spaces in it, better to surround all references like "$0".
如果您的 $0 可能会在其中包含空格,那么最好将所有引用括起来,例如"$0".
To be super-bullet proof, consider checking all command returns AND that Octal_Mode has a reasonable value
为了超级防弹,请考虑检查所有命令返回并且 Octal_Mode 具有合理的值
##代码##I hope this helps.
我希望这有帮助。
回答by JoeG
Very late answer here, but as I just solved this too, I thought it might help someone to post the approach:
这里的答案很晚,但由于我也刚刚解决了这个问题,我认为这可能有助于某人发布该方法:
##代码##
