如果在执行过程中编辑脚本会怎样?


31

我有一个普遍的问题,这可能是对Linux中如何处理进程的误解的结果。

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

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

运行脚本A:

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

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

那么,这是一个真正的问题,有什么办法可以在脚本A仍在运行时对其进行编辑?还是一旦开始执行便无法进行编辑?


2
我认为这取决于外壳。尽管您声明正在使用bash。看来这将取决于Shell内部加载脚本的方式。
13年

如果您提供文件而不是执行文件,则行为也可能会更改。
奋斗

1
我认为bash在执行之前会将整个脚本读入内存。
w4etwetewtwet

2
@handuel,不,不是。就像它不会等到在提示符下键入“ exit”开始解释您输入的命令一样。
斯特凡Chazelas

1
@StephaneChazelas是的,不是从终端读取的,但是这与运行脚本不同。
w4etwetewtwet

Answers:


21

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

这是行之有效的,因为unix确保(至少对于本地文件系统而言)打开的文件一直存在,直到关闭为止,即使该文件被“删除”并创建了一个具有相同名称的新文件。(实际上,unix系统对“删除”文件的调用实际上被称为“ unlink”。)因此,通常来说,如果外壳解释器打开了某些源文件,并且您以上述方式“编辑”该文件,由于它仍然打开了原始文件,因此外壳什至看不到更改。

[注:与所有基于标准的注释一样,以上内容可能有多种解释,并且存在各种不同的情况,例如NFS。欢迎学友在注释中添加例外。]

当然,可以直接修改文件。这对于编辑目的不是很方便,因为尽管您可以覆盖文件中的数据,但是如果不移动所有后续数据就无法删除或插入,这意味着需要进行大量重写。此外,当您执行此转换时,文件的内容将是不可预测的,打开文件的进程将受到影响。为了避免这种情况(例如,与数据库系统一样),您需要一套复杂的修改协议和分布式锁。这些东西远远超出了典型文件编辑实用程序的范围。

因此,如果要在外壳处理文件的同时编辑文件,则有两个选择:

  1. 您可以追加到文件。这应该一直有效。

  2. 您可以使用长度完全相同的新内容覆盖文件。根据外壳程序是否已经读取文件的那部分,这可能行不通。由于大多数文件I / O都涉及读取缓冲区,并且由于我所知道的所有外壳程序在执行前都读取了整个复合命令,因此您几乎不可能摆脱这种情况。这肯定是不可靠的。

我不知道Posix标准中的任何措辞,实际上这要求在执行文件时将其附加到脚本文件中,因此它可能不适用于每个符合Posix的Shell,而对于当前提供的几乎几乎所有内容来说,则更是如此。有时是posix兼容的shell。YMMV。但据我所知,它确实可以与bash一起使用。

作为证据,这是臭名昭著的99瓶bash啤酒程序的“无循环”实现,该程序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}   #

当我运行它时,它以“ No more”开头,然后继续为-1并无限期地变为负数。
Daniel Hershcovich 2013年

如果我export beer=100在运行脚本之前进行了操作,它将按预期工作。
Daniel Hershcovich 2013年

@DanielHershcovich:非常正确;我的草率测试。我想我已经解决了;现在,它需要一个可选的count参数。更好和更有趣的修复方法是,如果参数与缓存的副本不对应,则自动重置。
rici

18

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

例如在:

cmd1
cmd2

外壳程序将按块读取脚本,因此很可能读取了两个命令,解释了第一个命令,然后返回到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
}

外壳将必须读取结束符},将其存储在内存中并执行它。由于存在exit,shell将不再从脚本中读取内容,因此您可以在解释shell时安全地对其进行编辑。

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

要做到这一点,重命名the-script,以the-script.old复制the-script.oldthe-script和编辑它。


4

实际上,没有安全的方法可以在脚本运行时对其进行修改,因为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

我已通过捕获$0$@在脚本的开头并在脚本中使用这些变量来解决该问题exec
Drav Sloan
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.