如何从内部更新shell脚本

时间:2014-02-26 18:51:56

标签: linux bash shell debian exec

我想在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脚本的开头插入空行,结果会改变。

4 个答案:

答案 0 :(得分:2)

exectestB.sh使用copy命令覆盖它自己(执行时是exectest.sh)。这是不可能的。脚本继续被读取。 exectestA.sh应该执行'echo'命令,但由于字符偏移'ho'被读取,这不是命令。

答案 1 :(得分:0)

您所看到的行为肯定无法保证,特别是因为您在shebang行中有/bin/sh。在某些系统上,/bin/sh 不是 bash,并且行为可能不同。

bash尝试通过在执行命令之前记住脚本文件中的当前位置来处理自修改脚本,并在读取下一个命令之前寻找回到该位置。 (粗略地说。代码有点复杂,并且有一些很多的极端情况。如果脚本文件不是真正的文件,它将无法工作,因此无法进行搜索。)所以覆盖执行脚本文件后,执行将在新文件中的相同字符偏移处继续执行。为了使其有意义,您必须确保新文件与旧文件具有完全相同的前缀(或至少是完全相同长度的前缀)。

通过在同一命令中执行cpexec,可以在不依赖bash内部的情况下进行更新:

cp exectestB.sh exectest.sh && exec ./exectest.sh

请记住,exec永远不会返回,因此只有cpexec失败时,才会执行下一个命令(如果有的话)。

答案 2 :(得分:0)

我的第一反应是这听起来像XY Problem。你真正试图实现什么?

那就是说,你的成功取决于你正在使用的翻译。当您标记了问题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,因为每个脚本都是从头开始执行而不是从你要覆盖的脚本中的偏移量。

这就是你要找的东西吗?

答案 3 :(得分:0)

如果它能帮到任何人,我就是这样做的。如果现有脚本无法下载新脚本,它会执行现有脚本,否则它将替换为已下载的脚本并执行:

#!/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