【问题标题】:How to update a shell script from within itself如何从自身内部更新 shell 脚本
【发布时间】:2014-02-26 18:51:55
【问题描述】:

我想在 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

如果 cp 不存在于 exectestB 中,则没有问题(它在 b3 之后终止)。

虽然我知道对于正常的更新过程来说 B 中的 cp 是不需要的,但我想了解为什么在 B 中包含 cp 会导致错误以避免将来可能出现的错误。

目前,在我看来,execA 在调用 execB 之后继续运行。是这样吗,如果是,我该如何避免?

注意:这似乎取决于 B 脚本的长度。如果我在 B 脚本的开头插入空行,结果会发生变化。

【问题讨论】:

  • 避免更新当前的shell脚本......

标签: linux bash shell debian exec


【解决方案1】:

exectestB.sh 使用复制命令覆盖自身(执行时为 exectest.sh)。这是不可能的。脚本继续阅读。 exectestA.sh 应该执行命令'echo',但是因为字符偏移'ho'被读取,这不是命令。

【讨论】:

    【解决方案2】:

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

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

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

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

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

    【讨论】:

    • 谢谢,我发现它确实与 bash vs. dash 有关。这个测试用例在 dash 和 bash 上的表现非常不同。 pastie.org/8804637
    【解决方案3】:

    如果它可以帮助任何人,我就是这样做的。如果无法下载新脚本,它将执行现有脚本,否则它将用下载的脚本替换自己并执行:

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

    【讨论】:

      【解决方案4】:

      我的第一反应是这听起来像是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 的目的是用新的过程映像“替换”当前过程映像。 bashexec 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,因为每个脚本都是从头开始执行,而不是从您要覆盖的脚本中的偏移量开始执行。

      这就是你要找的吗?

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-09-23
        • 1970-01-01
        • 2020-02-25
        • 2012-11-28
        相关资源
        最近更新 更多