如何在执行之前读取整个shell脚本?


35

通常,如果您编辑脚本,则脚本的所有运行用法都容易出错。

据我了解,bash(也有其他shell吗?)以增量方式读取脚本,因此,如果从外部修改脚本文件,它将开始读取错误的内容。有什么办法可以防止呢?

例:

sleep 20

echo test

如果执行此脚本,bash将读取第一行(例如10个字节)并进入睡眠状态。恢复时,脚本中可能有不同的内容(从第10个字节开始)。我可能在新脚本的一行中间。因此,正在运行的脚本将被破坏。


“在外部修改脚本”是什么意思?
maulinglawns

1
也许有一种方法可以将所有内容包装在一个函数中或某些东西中,所以shell将首先读取整个脚本?但是调用函数的最后一行呢,它将被读取到EOF为止?也许省略最后一个\n可以解决问题?也许一个子壳()会做?我对它不是很有经验,请帮忙!
VasyaNovikov '16

@maulinglawns如果脚本具有类似内容,sleep 20 ;\n echo test ;\n sleep 20而我开始对其进行编辑,则可能行为不正确。例如,bash可以读取脚本的前10个字节,了解sleep命令并进入睡眠状态。恢复后,文件中的内容将以10个字节开始。
VasyaNovikov '16

1
那么,您的意思是您正在编辑正在执行的脚本?首先停止脚本,进行编辑,然后再次启动。
maulinglawns

@maulinglawns是的,基本上就是这样。问题是,对于我来说停止脚本并不方便,而且很难总是记住这样做。也许有一种方法可以强制bash首先阅读整个脚本?
VasyaNovikov '16

Answers:


43

是的,外壳程序bash尤其要小心,一次读取一行,因此它的工作方式与交互式使用时相同。

您会注意到,当文件不可搜索时(例如管道),bash甚至一次只能读取一个字节,以确保不会读取\n字符以外的内容。当可查找文件时,它会通过一次读取完整的块进行优化,但会在之后查找\n

这意味着您可以执行以下操作:

bash << \EOF
read var
var's content
echo "$var"
EOF

或编写可自我更新的脚本。如果没有保证,您将无法做到。

现在,很少有人愿意做那样的事情,而且,正如您所发现的那样,该功能往往比有用的功能妨碍更多的工作。

为了避免这种情况,您可以尝试确保不要就地修改文件(例如,修改副本,然后就地移动副本(例如sed -iperl -pi,某些编辑器会这样做))。

或者您可以像这样编写脚本:

{
  sleep 20
  echo test
}; exit

(请注意,exit}; 处于同一行很重要,尽管您也可以将其放在右括号的右方)。

要么:

main() {
  sleep 20
  echo test
}
main "$@"; exit

exit在开始执行任何操作之前,shell将需要先读取脚本。这样可以确保外壳程序不会再次从脚本读取。

这意味着整个脚本将存储在内存中。

这也可能影响脚本的解析。

例如,在bash

export LC_ALL=fr_FR.UTF-8
echo $'St\ue9phane'

将输出U + 00E9以UTF-8编码的格式。但是,如果将其更改为:

{
  export LC_ALL=fr_FR.UTF-8
  echo $'St\ue9phane'
}

\ue9将在实际上是在当时该命令被解析在这种情况下是字符集进行扩展之前export执行命令。

还要注意,如果使用sourceaka .命令,并且在某些shell中,源文件也会遇到同样的问题。

bash尽管不是他的source命令在解释文件之前就完全读取了文件,但情况并非如此。如果要bash专门编写,则可以通过在脚本开始处添加来实际使用它:

if [[ ! $already_sourced ]]; then
  already_sourced=1
  source "$0"; exit
fi

(尽管您可以想象将来的版本bash会改变这种行为,但我不会依赖它,这种行为目前可以看作是一种限制(bash和AT&T ksh是唯一的POSIX式外壳,其行为可以这么说)而且这个already_sourced技巧有点脆弱,因为它假定变量不在环境中,更不用说它会影响BASH_SOURCE变量的内容了。


@VasyaNovikov,目前SE似乎有问题(或至少对我而言)。当我添加我的时,只有几个答案,尽管您的评论似乎只出现了,即使它说它是16分钟前发布的(或者也许是我丢了大理石)。无论如何,请注意此处需要额外的“退出”,以避免文件大小增加时出现问题(如我在您的答案中添加的注释中所述)。
斯特凡Chazelas

斯特凡,我想我已经找到了另一种解决方案。是使用}; exec true。这样,文件末尾不需要换行,这对某些编辑器(例如emacs)很友好。我认为可以正常使用的所有测试}; exec true
VasyaNovikov

@VasyaNovikov,不确定您的意思。比}; exit什么更好?您还将失去退出状态。
斯特凡Chazelas

如另一个问题所述:通常先解析整个文件,然后在使用点命令(. script)的情况下执行复合语句。
schily

@schily,是的,我在此答案中提到作为AT&T ksh和bash的限制。其他POSIX类型的外壳没有此限制。
斯特凡Chazelas

12

您只需要删除文件(即复制,删除,将副本重命名回原始名称)。实际上,可以配置许多编辑器来为您执行此操作。当您编辑文件并将更改的缓冲区保存到该文件时,它不会重命名该文件,而是会重命名该旧文件,创建一个新文件,然后将新内容放入新文件中。因此,任何正在运行的脚本都应继续正常运行。

通过使用像RCS这样的简单版本控制系统,该版本控制系统可随时用于vim和emacs,您将获得具有更改历史记录的双重优势,并且结帐系统应默认情况下删除当前文件并以正确的模式重新创建它。(当然要注意不要硬链接此类文件)。


“删除”实际上不是该过程的一部分。如果要使其正确原子化,请在目标文件上进行重命名-如果您有删除步骤,则存在删除后但重命名前进程死掉的风险,根本不保留文件(或读者尝试在该窗口中访问文件,并且找不到可用的旧版本或新版本)。
查尔斯·达菲

11

最简单的解决方案:

{
  ... your code ...

  exit
}

这样,bash将{}在执行之前读取整个块,并且该exit指令将确保在代码块之外不会读取任何内容。

如果您不想“执行”脚本,而是想要“源”脚本,则需要其他解决方案。然后,这应该起作用:

{
  ... your code ...

  return 2>/dev/null || exit
}

或者,如果您想直接控制退出代码:

{
  ... your code ...

  ret="$?";return "$ret" 2>/dev/null || exit "$ret"
}

瞧!该脚本可以安全地进行编辑,源和执行。您仍然必须确保在最初读取它的那几毫秒内没有修改它。


1
我发现它看不到EOF并停止读取文件,但是它在其“缓冲流”处理中纠结在一起并最终经过了文件末尾,这就是为什么如果文件增加的幅度不大,但是当您使文件的大小超过以前的两倍时,外观看起来很糟。我会尽快向bash维护人员报告错误。
斯特凡Chazelas


评论不作进一步讨论;此对话已转移至聊天
terdon

5

概念证明。这是一个可以自我修改的脚本:

cat <<EOF >/tmp/scr
#!/bin/bash
sed  s/[k]ept/changed/  /tmp/scr > /tmp/scr2

# this next line overwites the on disk copy of the script
cat /tmp/scr2 > /tmp/scr
# this line ends up changed.
echo script content kept
EOF
chmod u+x /tmp/scr
/tmp/scr

我们看到更改的版本打印

这是因为bash加载使文件句柄保持打开状态,因此可以立即看到对该文件的更改。

如果您不想更新内存中的副本,请取消链接原始文件并替换它。

一种方法是使用sed -i。

sed -i '' filename

概念证明

cat <<EOF >/tmp/scr
#!/bin/bash
sed  s/[k]ept/changed/  /tmp/scr > /tmp/scr2

# this next line unlinks the original and creates a new copy.
sed -i ''  /tmp/scr

# now overwriting it has no immediate effect
cat /tmp/scr2 > /tmp/scr
echo script content kept
EOF

chmod u+x /tmp/scr
/tmp/scr

如果使用编辑器更改脚本,则可能需要启用“保留备份副本”功能,以使编辑器将更改后的版本写入新文件,而不是覆盖现有文件。


2
否,bash不会使用打开文件mmap()。只需按需一次读取一行,就像在交互时从终端设备获取命令时一样。
斯特凡Chazelas

2

将脚本包装在一个块{}中可能是最好的选择,但需要更改脚本。

F=$(mktemp) && cp test.sh $F && bash $F; rm $F;

将是第二好的选择(假设tmpfs),缺点是如果脚本使用$ 0则会破坏$ 0。

使用类似的东西F=test.sh; tail -n $(cat "$F" | wc -l) "$F" | bash不太理想,因为它必须将整个文件保留在内存中并破坏$ 0。

应避免触摸原始文件,以免影响上次修改时间,读锁和硬链接。这样,您可以在运行文件时使编辑器保持打开状态,并且rsync不会不必要地对文件进行校验和以进行备份,并且硬链接可以按预期运行。

在编辑时替换文件是可以的,但是不够健壮,因为它对其他脚本/用户不可执行,或者可能会忘记。再次,它将打破硬链接。


任何能够复制的东西都可以使用。tac test.sh | tac | bash
Jasen
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.