如果在执行期间编辑脚本会发生什么?

如果在执行期间编辑脚本会发生什么?

我有一个一般性问题,这可能是由于对 Linux 中进程的处理方式的误解所致。

出于我的目的,我将定义一个“脚本”作为保存到文本文件的 bash 代码片段,并为当前用户启用执行权限。

我有一系列相互调用的脚本。为了简单起见,我将它们称为脚本 A、B 和 C。脚本 A 执行一系列语句,然后暂停,然后执行脚本 B,然后暂停,然后执行脚本 C。换句话说,这一系列语句步骤是这样的:

运行脚本A:

  1. 系列声明
  2. 暂停
  3. 运行脚本B
  4. 暂停
  5. 运行脚本C

我从经验中知道,如果我运行脚本 A 直到第一次暂停,然后在脚本 B 中进行编辑,那么当我允许代码恢复时,这些编辑就会反映在代码的执行中。同样,如果我在脚本 A 仍暂停时对脚本 C 进行编辑,然后允许其在保存更改后继续,这些更改将反映在代码的执行中。

那么真正的问题是,有没有办法在脚本 A 仍在运行时对其进行编辑?或者一旦开始执行就无法编辑?

答案1

在 Unix 中,大多数编辑器的工作方式是创建一个包含编辑内容的新临时文件。保存编辑后的文件时,原始文件将被删除,临时文件将重命名为原始名称。 (当然,有各种保护措施来防止数据丢失。)例如,这是由(“就地”)标志使用sedperl使用(“就地”)标志调用时的样式-i,它根本不是真正的“就地”。应该叫“新地旧名”。

这很有效,因为 unix 确保(至少对于本地文件系统)打开的文件将继续存在,直到它被关闭,即使它被“删除”并创建了一个具有相同名称的新文件。 (unix 系统调用“删除”文件实际上被称为“取消链接”,这并非巧合。)因此,一般来说,如果 shell 解释器打开了某个源文件,并且您以上述方式“编辑”该文件,shell 甚至不会看到更改,因为它仍然打开着原始文件。

[注意:与所有基于标准的注释一样,上述内容有多种解释,并且存在各种极端情况,例如 NFS。欢迎学究们补充评论,但有例外。]

当然,也可以直接修改文件;只是对于编辑目的不太方便,因为虽然您可以覆盖文件中的数据,但在不移动所有后续数据的情况下无法删除或插入,这意味着相当多的重写。此外,当您进行这种转换时,文件的内容将是不可预测的,并且打开该文件的进程将受到影响。为了摆脱这个问题(例如数据库系统),您需要一套复杂的修改协议和分布式锁;这远远超出了典型文件编辑实用程序的范围。

因此,如果您想在 shell 处理文件时对其进行编辑,您有两个选择:

  1. 您可以追加到文件中。这应该总是有效。

  2. 您可以用新内容覆盖该文件长度完全相同。这可能有效,也可能无效,具体取决于 shell 是否已经读取了文件的该部分。由于大多数文件 I/O 涉及读取缓冲区,并且由于我所知道的所有 shell 在执行它之前都会读取整个复合命令,因此您不太可能摆脱这种情况。这肯定不可靠。

我不知道 Posix 标准中的任何措辞实际上需要在执行文件时附加到脚本文件的可能性,因此它可能不适用于每个 Posix 兼容的 shell,更不用说当前提供的几乎-以及有时兼容 posix 的 shell。所以YMMV。但据我所知,它确实可以在 bash 中可靠地工作。

作为证据,这里是 bash 中臭名昭著的 99 瓶啤酒程序的“无循环”实现,它使用dd覆盖和追加(覆盖可能是安全的,因为它替换了当前执行的行,该行始终是文件,具有完全相同长度的注释;我这样做是为了可以在没有自我修改行为的情况下执行最终结果。)

#!/bin/bash
if [[ $1 == reset ]]; then
  printf "%s\n%-16s#\n" '####' 'next ${1:-99}' |
  dd if=/dev/stdin of=$0 seek=$(grep -bom1 ^#### $0 | cut -f1 -d:) bs=1 2>/dev/null
  exit
fi

step() {
  s=s
  one=one
  case $beer in
    2) beer=1; unset s;;
    1) beer="No more"; one=it;;
    "No more") beer=99; return 1;;
    *) ((--beer));;
  esac
}
next() {
  step ${beer:=$(($1+1))}
  refrain |
  dd if=/dev/stdin of=$0 seek=$(grep -bom1 ^next\  $0 | cut -f1 -d:) bs=1 conv=notrunc 2>/dev/null
}
refrain() {
  printf "%-17s\n" "# $beer bottles"
  echo echo ${beer:-No more} bottle$s of beer on the wall, ${beer:-No more} bottle$s of beer.
  if step; then
    echo echo Take $one down, pass it around, $beer bottle$s of beer on the wall.
    echo echo
    echo next abcdefghijkl
  else
    echo echo Go to the store, buy some more, $beer bottle$s of beer on the wall.
  fi
}
####
next ${1:-99}   #

答案2

bash确保它在执行命令之前读取命令有很长的路要走。

例如在:

cmd1
cmd2

shell 将按块读取脚本,因此可能会读取两个命令,解释第一个命令,然后返回到脚本的末尾cmd1并再次读取脚本以读取cmd2并执行它。

您可以轻松验证它:

$ cat a
echo foo | dd 2> /dev/null bs=1 seek=50 of=a
echo bar
$ bash a
foo

(虽然看看strace输出,它似乎做了一些更奇特的事情(比如多次读取数据,向后查找......)比我几年前尝试相同的时候,所以我上面关于向后查找的陈述可能不再适用于新版本)。

但是,如果您将脚本编写为:

{
  cmd1
  cmd2
  exit
}

shell 必须读取直到结束的内容},将其存储在内存中并执行它。由于exit,shell 不会再次读取脚本,因此您可以在 shell 解释脚本时安全地编辑它。

或者,在编辑脚本时,请确保编写脚本的新副本。 shell 将继续读取原始文件(即使它被删除或重命名)。

为此,请重命名the-scriptthe-script.old、复制并the-script.old编辑the-script它。

答案3

实际上没有安全的方法可以在脚本运行时修改脚本,因为 shell 可以使用缓冲来读取文件。此外,如果通过用新文件替换脚本来修改脚本,则 shell 通常只会在执行某些操作后读取新文件。

通常,当脚本在执行时发生更改时,shell 最终会报告语法错误。这是因为,当 shell 关闭并重新打开脚本文件时,它会使用文件中的字节偏移量在返回时重新定位自身。

答案4

您可以通过在脚本上设置陷阱,然后使用exec来获取新的脚本内容来解决此问题。但请注意,该exec调用从头开始启动脚本,而不是从运行过程中到达的位置开始,因此脚本 B 将被调用(依此类推)。

#! /bin/bash

CMD="$0"
ARGS=("$@")

trap reexec 1

reexec() {
    exec "$CMD" "${ARGS[@]}"
}

while : ; do sleep 1 ; clear ; date ; done

这将继续在屏幕上显示日期。然后我可以编辑我的脚本并更改dateecho "Date: $(date)".写出后,运行脚本仍然只显示日期。但是,如果我发送设置trap为捕获的信号,脚本将exec(用指定的命令替换当前正在运行的进程)即命令$CMD和参数$@。您可以通过发出kill -1 PID- 其中 PID 是正在运行的脚本的 PID - 来完成此操作,并且输出将更改为在命令输出Date:之前显示date

您可以将脚本的“状态”存储在外部文件(例如 /tmp)中,并读取内容以了解程序重新执行时在哪里“恢复”。然后,您可以添加额外的陷阱终止 (SIGINT/SIGQUIT/SIGKILL/SIGTERM) 来清除该 tmp 文件,这样当您在中断“脚本 A”后重新启动时,它将从头开始。有状态的版本会是这样的:

#! /bin/bash

trap reexec 1
trap cleanup 2 3 9 15

CMD="$0"
ARGS=("$@")
statefile='/tmp/scriptA.state'
EXIT=1

reexec() { echo "Restarting..." ; exec "$CMD" "${ARGS[@]}"; }
cleanup() { rm -f $statefile; exit $EXIT; }
run_scriptB() { /path/to/scriptB; echo "scriptC" > $statefile; }
run_scriptC() { /path/to/scriptC; echo "stop" > $statefile;  }

while [ "$state" != "stop" ] ; do

    if [ -f "$statefile" ] ; then
        state="$(cat "$statefile")"
    else
        state='starting'
    fi

    case "$state" in
        starting)         
            run_scriptB
        ;;
        scriptC)
            run_scriptC
        ;;
    esac
done

EXIT=0
cleanup

相关内容