我想在shell脚本中实现一个自动更新程序。这个脚本将首先更新自己,然后继续进行其余的更新。
我相信在Linux中使用exec
命令是可能的,但在我看来,这只是调用一个新脚本,而不是停止终止旧脚本。
你能告诉我为什么在下面的场景中出现错误吗。
exectestA.sh
#!/bin/sh
echo "a1"
sleep 1
cp exectestB.sh exectest.sh
echo "a2"
sleep 1
exec ./exectest.sh
echo "a3"
sleep 1
exectestB.sh
#!/bin/sh
echo "b1"
sleep 1
echo "b2"
sleep 1
cp exectestA.sh exectest.sh
echo "b3"
sleep 1
启动命令
cp exectestA.sh exectest.sh
./exectest.sh
我的机器上的输出
a1
a2
b1
b2
b3
./exectest.sh: 13: ./exectest.sh: ho: not found
如果exectestB中不存在cp,则没有问题(它在b3之后终止)。
虽然我知道对于正常的更新过程来说,不应该需要B中的cp,但我想理解为什么在B中包含cp会导致错误,以避免将来可能出现的错误。
目前,在我看来,execA在调用execB后仍在运行。是这样吗?如果是,我该如何避免?
注意:这似乎取决于B脚本的长度。如果我在B脚本的开头插入空行,结果就会改变。
exectestB.sh正在用copy命令覆盖它自己(执行时它是exectest.sh)。这是不可能的。脚本将继续读取。exectestA.sh应该执行命令"echo",但由于字符偏移量"ho"为read,这不是一个命令。
您看到的行为肯定不能保证,特别是因为在shebang行中有/bin/sh
。在某些系统上,/bin/sh
是而不是bash,并且可能不会以相同的方式进行操作。
bash
试图通过在执行命令之前记住脚本文件中的当前位置,并在读取下一个命令之前查找回该位置来处理自修改脚本。(粗略地说。代码有点复杂,有很多的角案例。如果脚本文件不是真实的文件,它就不起作用,因此无法进行查找。)因此,在覆盖正在执行的脚本文件后,将在新文件中以相同的字符偏移量继续执行。为了使其有意义,您必须确保新文件具有与旧文件完全相同的前缀(或者至少具有完全相同长度的前缀)。
通过在同一命令中执行cp
和exec
,可以在不依赖bash内部的情况下进行更新:
cp exectestB.sh exectest.sh && exec ./exectest.sh
请记住,exec
永远不会返回,因此只有在cp
或exec
失败时才会执行下一个命令(如果有)。
如果它能帮助任何人,下面是我的做法。如果无法下载新的脚本,它会执行现有的脚本,否则它会用下载的脚本替换自己并执行:
#!/usr/bin/env bash
#
set -fb
readonly THISDIR=$(cd "$(dirname "$0")" ; pwd)
readonly MY_NAME=$(basename "$0")
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=$1
readonly OUTPUT_FILENAME=$2
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
我的第一反应是这听起来像是一个XY问题。你真正想达到的目标是什么?
也就是说,你在这方面的成功取决于你使用的口译员。当您将问题标记为bash
时,您正在以#!/bin/sh
的形式执行脚本。
当我使用bash运行您的测试时,我会得到类似的结果。但当我使用FreeBSD附带的/bin/sh
运行它时,我得到了更像成功的东西:
0 ghoti@pc:~/tmp 6> ./exectest.sh
a1
a2
b1
b2
b3
a3
0 ghoti@pc:~/tmp 7>
请记住,exec
的目的是用新的过程映像"替换"当前过程映像。根据手册页,bash中exec
shell命令的行为是"替换shell"。
我认为如果你不尝试替换实际运行的脚本,你会得到最好的结果。例如:
#!/bin/bash
echo "A1 - $0"
sleep 1
echo "A2"
sleep 1
me=$(basename $0); me=A-${me##?-}
tr 'AB' 'BA' < $0 > $me
chmod +x $me
exec ./$me
echo "A3"
sleep 1
对我来说,它给出了以下重复的结果:
0 ghoti@pc:~/tmp 17> ./exectest.sh
A1 - ./exectest.sh
A2
B1 - /home/ghoti/tmp/A-exectest.sh
B2
A1 - /home/ghoti/tmp/B-exectest.sh
A2
B1 - /home/ghoti/tmp/A-exectest.sh
B2
A1 - /home/ghoti/tmp/B-exectest.sh
^C
1 ghoti@pc:~/tmp 18>
这会在A和B脚本之间来回反弹,永远不会达到A3,因为每个脚本都是从开始执行的,而不是从覆盖的脚本中的偏移量执行的。
这就是你要找的吗?