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

Vas*_*kov 35 shell

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

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

例子:

sleep 20

echo test
Run Code Online (Sandbox Code Playgroud)

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

Sté*_*las 43

是的,shellbash尤其会小心地一次读取一行文件,因此它的工作方式与交互使用时的工作方式相同。

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

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

bash << \EOF
read var
var's content
echo "$var"
EOF
Run Code Online (Sandbox Code Playgroud)

或者编写自己更新的脚本。如果它没有给你那个保证,你将无法做到这一点。

现在,您很少想要做这样的事情,而且正如您发现的那样,该功能往往会比它有用的多。

为避免这种情况,您可以尝试确保不要就地修改文件(例如,修改副本,并将副本移动到位(例如sed -iperl -pi和某些编辑器所做的))。

或者您可以编写如下脚本:

{
  sleep 20
  echo test
}; exit
Run Code Online (Sandbox Code Playgroud)

(请注意,exit};位于同一行是很重要的,尽管您也可以将它放在大括号内,就在关闭大括号之前)。

或者:

main() {
  sleep 20
  echo test
}
main "$@"; exit
Run Code Online (Sandbox Code Playgroud)

exit在开始做任何事情之前,shell 将需要读取脚本。这确保 shell 不会再次从脚本中读取。

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

这也会影响脚本的解析。

例如,在bash

export LC_ALL=fr_FR.UTF-8
echo $'St\ue9phane'
Run Code Online (Sandbox Code Playgroud)

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

{
  export LC_ALL=fr_FR.UTF-8
  echo $'St\ue9phane'
}
Run Code Online (Sandbox Code Playgroud)

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

另请注意,如果使用sourceaka.命令,对于某些 shell,您将遇到相同类型的源文件问题。

bash虽然谁的source命令在解释文件之前完全读取文件,但情况并非如此。如果bash专门编写,您实际上可以通过在脚本开头添加来使用它:

if [[ ! $already_sourced ]]; then
  already_sourced=1
  source "$0"; exit
fi
Run Code Online (Sandbox Code Playgroud)

(我不会依赖它,因为你可以想象未来的版本bash可能会改变目前被视为一种限制的行为(bash 和 AT&T ksh 是唯一的类似 POSIX 的 shell,据所知)并且这个already_sourced技巧有点脆弱,因为它假设变量不在环境中,更不用说它会影响 BASH_SOURCE 变量的内容)


meu*_*euh 12

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

通过使用像 RCS 这样的简单版本控制系统,它对 vim 和 emacs 很容易使用,您可以获得具有更改历史记录的双重优势,并且检出系统默认应删除当前文件并使用正确的模式重新创建它。(当然要小心硬链接这些文件)。


Vas*_*kov 11

最简单的解决方案:

{
  ... your code ...

  exit
}
Run Code Online (Sandbox Code Playgroud)

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

如果您不想“执行”脚本,而是“源”它,则需要不同的解决方案。这应该工作:

{
  ... your code ...

  return 2>/dev/null || exit
}
Run Code Online (Sandbox Code Playgroud)

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

{
  ... your code ...

  ret="$?";return "$ret" 2>/dev/null || exit "$ret"
}
Run Code Online (Sandbox Code Playgroud)

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


Jas*_*sen 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
Run Code Online (Sandbox Code Playgroud)

我们看到更改后的版本打印

这是因为 bash 加载保留了一个文件句柄以打开脚本,因此将立即看到对文件的更改。

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

一种方法是使用 sed -i。

sed -i '' filename
Run Code Online (Sandbox Code Playgroud)

概念证明

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
Run Code Online (Sandbox Code Playgroud)

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

  • 不,`bash` 不会用 `mmap()` 打开文件。根据需要一次读取一行是很小心的,就像在交互时从终端设备获取命令一样。 (2认同)