我有以下rc.local
脚本:
#!/bin/sh -e
#
# rc.local
#
# This script is executed at the end of each multiuser runlevel.
# Make sure that the script will "exit 0" on success or any other
# value on error.
#
# In order to enable or disable this script just change the execution
# bits.
#
# By default this script does nothing.
sh /home/incero/startup_script.sh
/bin/chmod +x /script.sh
sh /script.sh
exit 0
第一行,startup_script.sh
实际上下载的是第三行所指的script.sh
文件。/script.sh
不幸的是,它似乎没有使脚本可执行或运行脚本。rc.local
启动后手动运行文件可以完美运行。chmod 不能在启动时运行吗?
答案1
您可以直接跳至快速修复但这不一定是最好的选择。所以我建议先读完这些。
rc.local
不容忍错误。
rc.local
不提供智能地从错误中恢复的方法。如果任何命令失败,它将停止运行。第一行,#!/bin/sh -e
,导致它在用标志调用的 shell 中执行-e
。-e
标志是使脚本(在本例中为rc.local
)在其中的命令第一次失败时停止运行的原因。
您希望rc.local
这样做。如果命令失败,您可以不是希望它能够继续执行依赖于它成功的任何其他启动命令。
因此,如果任何命令失败,后续命令将不会运行。这里的问题是/script.sh
没有运行(不是失败,见下文),所以很可能是它之前的某个命令失败了。但是哪一个呢?
是吗/bin/chmod +x /script.sh
?
不。
chmod
任何时候都可以正常运行。只要包含的文件系统/bin
已挂载,您就可以运行/bin/chmod
。并且已在运行/bin
之前挂载rc.local
。
以 root 身份运行时,/bin/chmod
很少会失败。如果它操作的文件是只读的,则会失败,并且可能如果文件系统不支持权限,则失败。这里不太可能出现这种情况。
顺便说一下sh -e
,仅有的原因是如果失败,它实际上会成为一个问题chmod
。当您通过显式调用其解释器来运行脚本文件时,文件是否标记为可执行文件并不重要。只有当它说/script.sh
该文件的可执行位才重要。因为它说sh /script.sh
,所以它不重要(当然,除非/script.sh
调用自己在运行时,这可能会由于不可执行而失败,但它不太可能调用自身)。
那么什么失败了?
sh /home/incero/startup_script.sh
失败了。几乎可以肯定。
我们知道它运行了,因为它下载了/script.sh
。
(否则,确保它确实运行就很重要,以防/bin
万一小路--rc.local
不一定PATH
与您登录时拥有的相同。如果/bin
不在rc.local
的路径中,则需要sh
以 的身份运行/bin/sh
。由于它确实运行了,/bin
所以在 中PATH
,这意味着您可以运行位于 中的其他命令/bin
,而无需完全限定其名称。例如,您可以只运行chmod
而不是/bin/chmod
。但是,为了符合 中的风格rc.local
,我对除 之外的所有命令都使用了完全限定的名称sh
,每当我建议您运行它们时。)
我们可以肯定/bin/chmod +x /script.sh
它从未运行过(或者您会看到它/script.sh
被执行了)。而且我们也知道sh /script.sh
它也没有运行。
但是它下载了/script.sh
。成功了!怎么会失败呢?
成功的两种含义
当一个人说命令成功时,他/她可能有两种不同的含义:
- 它做了你想让它做的事。
- 据报道,这一行动取得了成功。
失败也是如此。当一个人说命令失败时,可能意味着:
- 它并没有做你想让它做的事。
- 据报道,这一计划失败了。
使用 运行的脚本sh -e
,例如rc.local
,将在第一次命令时停止运行报告称失败。命令实际上执行的操作没有区别。
除非您打算startup_script.sh
在它执行所需的操作时报告失败,否则这是一个错误startup_script.sh
。
它很可能已经startup_script.sh
完成了它应该做的一切,除了报告失败了。
如何报告成功或失败
脚本是零个或多个命令的列表。每个命令都有一个退出状态。假设实际运行脚本时没有失败(例如,如果解释器在运行脚本时无法读取脚本的下一行),则脚本的退出状态为:
0
(成功)如果脚本为空白(即没有命令)。N
,如果脚本因命令而结束,其中exit N
N
是一些退出代码。- 否则,为脚本中运行的最后一个命令的退出代码。
当可执行文件运行时,它会报告自己的退出代码——它们不仅仅适用于脚本。(从技术上讲,脚本的退出代码是运行它们的 shell 返回的退出代码。)
例如,如果 C 程序以 结尾exit(0);
,或者return 0;
在其main()
函数中,则代码0
将提供给操作系统,操作系统将其提供给调用进程(例如,可能是运行该程序的 shell)。
0
表示程序成功。其他数字表示程序失败。(因此,不同的数字有时可能表示程序失败的不同原因。)
注定失败的命令
有时,您运行程序时会故意让它失败。在这种情况下,您可能会认为它的失败是成功的,即使程序报告失败并不是错误。例如,您可能会对您rm
怀疑不存在的文件使用,只是为了确保它已被删除。
类似的事情可能发生在 中startup_script.sh
,就在它停止运行之前。最后运行的命令脚本中的 可能会报告失败(即使它的“失败”可能完全没问题甚至是必要的),这会导致脚本报告失败。
注定失败的测试
有一种特殊的命令是一个测试,我的意思是命令的运行是为了它的返回值,而不是它的副作用。也就是说,测试是一个命令,它是为了检查其退出状态(并对其采取行动)而运行的。
例如,假设我忘记了 4 是否等于 5。幸运的是,我懂 shell 脚本:
if [ 4 -eq 5 ]; then
echo "Yeah, they're totally the same."
fi
这里,测试[ -eq 5 ]
失败,因为结果 4 ≠ 5。这并不意味着测试没有正确执行;它确实正确执行了。它的工作是检查 4 是否 = 5,如果是则报告成功,否则报告失败。
你看,在 shell 脚本中,成功也可能意味着真的,失败也可能意味着错误的。
即使该echo
语句从未运行,整个if
块确实会返回成功。
不过,如果我写得更短一些的话:
[ 4 -eq 5 ] && echo "Yeah, they're totally the same."
这是常见的简写。&&
是布尔值 和运算符。&&
表达式由两边的 with 语句组成&&
,除非两边都返回 true(成功),否则返回 false(失败)。就像普通的和。
如果有人问你,“德里克是否去过商场并且想过蝴蝶?”而你知道德里克没有去过商场,你就不必费心去弄清楚他是否想过蝴蝶。
类似地,如果 左侧的命令&&
失败(false),则整个&&
表达式立即失败(false)。 右侧的语句&&
永远不会运行。
这里,[ 4 -eq 5 ]
运行。它“失败”(返回 false)。因此整个&&
表达式失败。echo "Yeah, they're totally the same."
永远不会运行。一切都按预期运行,但此命令报告失败(即使if
上面的其他等效条件报告成功)。
如果这是脚本中的最后一条语句(并且脚本到达了它,而不是在它之前的某个点终止),整个脚本将报告失败。
除此之外,还有很多测试。例如,有带||
(“或”)的测试。但是,上述示例足以解释什么是测试,并使您能够有效地使用文档来确定特定语句/命令是否是测试。
sh
与sh -e
,重温
自从线#!
(也可以看看这个问题/etc/rc.local
) 位于has的顶部sh -e
,操作系统将运行该脚本,就像使用以下命令调用它一样:
sh -e /etc/rc.local
相反,您的其他脚本(例如startup_script.sh
)运行没有旗帜-e
:
sh /home/incero/startup_script.sh
因此,即使其中的命令报告失败,它们仍会继续运行。
这是正常且良好的。rc.local
应该被调用sh -e
,而大多数其他脚本——包括大多数由rc.local
——运行的脚本不应该。
请务必记住以下区别:
sh -e
当脚本所包含的命令第一次退出并报告失败时,脚本运行并退出并报告失败。就好像脚本是一个由脚本中的所有命令通过
&&
运算符连接而成的单个长命令。sh
使用(不使用)运行的脚本-e
会继续运行,直到遇到终止(退出)它们的命令,或者运行到脚本的最后。每个命令的成功或失败本质上无关紧要(除非下一个命令检查它)。脚本以最后运行的命令的退出状态退出。
帮助你的剧本理解这并不是一次失败
当脚本没有失败时,如何防止它认为它失败了?
您可以观察一下在它运行结束之前发生的情况。
如果命令应该成功但却失败了,请找出原因并解决问题。
如果命令失败,并且这是正确的事,则阻止该失败状态传播。
防止失败状态传播的一种方法是运行另一个成功的命令。
/bin/true
没有副作用并报告成功(就像/bin/false
什么都不做也会失败)。另一个是确保脚本已终止
exit 0
。这不一定与
exit 0
位于脚本末尾相同。例如,if
脚本可能退出于其中的 -block。
在让脚本报告成功之前,最好先了解导致脚本报告失败的原因。如果脚本确实以某种方式失败(没有执行您希望它执行的操作),您实际上并不希望它报告成功。
快速修复
如果您无法让startup_script.sh
退出报告成功,您可以更改运行它的命令rc.local
,这样即使startup_script.sh
没有,该命令也会报告成功。
目前您有:
sh /home/incero/startup_script.sh
此命令具有相同的副作用(即运行的副作用startup_script.sh
),但总是报告成功:
sh /home/incero/startup_script.sh || /bin/true
记住,最好知道startup_script.sh
报告失败的原因并修复它。
快速修复如何工作
这实际上是一个测试的例子||
,或者测试。
假设你问我是否倒了垃圾或者是否梳理过猫头鹰的毛发。如果我倒了垃圾,我可以如实回答“是”,即使我不记得我是否梳理过猫头鹰的毛发。
左侧的命令||
将运行。如果成功(真),则右侧的命令不必运行。因此,如果startup_script.sh
报告成功,则该true
命令永远不会运行。
但是,如果startup_script.sh
报告失败[我没有倒垃圾],那么结果/bin/true
[如果我刷了猫头鹰]事。
/bin/true
总是返回成功(或真的(我们有时这样称呼它)。因此,整个命令成功,并且 中的下一个命令rc.local
可以运行。
关于成功/失败、真/假、零/非零的附加说明。
请忽略此内容。不过,如果您使用多种语言进行编程(即不仅仅是 shell 脚本),则可能需要阅读此内容。
对于使用 C 等编程语言的 Shell 脚本编写者以及使用 Shell 脚本的 C 程序员来说,他们非常困惑于发现:
在 shell 脚本中:
- 返回值
0
意味着成功和/或真的。 - 返回值不是
0
手段失败和/或错误的。
- 返回值
在 C 编程中:
- 返回值
0
意味着错误的.. - 返回值不是
0
手段真的。 - 对于成功和失败,并没有简单的规则。有时
0
表示成功,有时表示失败,有时表示刚刚添加的两个数字的总和为零。在一般编程中,返回值用于表示各种不同类型的信息。 - 根据 shell 脚本的规则,程序的数字退出状态应指示成功或失败。也就是说,即使
0
C 程序中的 means 为 false,0
如果希望程序报告成功(shell 随后将其解释为),仍应让程序返回其退出代码真的)。
- 返回值
答案2
您可以(在我使用的 Unix 变体中)通过更改以下内容来覆盖默认行为:
#!/bin/sh -e
到:
#!/bin/sh
在脚本的开头。“-e”标志指示 sh 在第一次出现错误时退出。该标志的长名称是“errexit”,因此原始行相当于:
#!/bin/sh --errexit
答案3
@Bryce 的回答在某些情况下是完全没问题的——取决于你用它做什么rc.local
。如果在第一次错误之后什么都不发生是绝对关键的,那当然-e
就是你想要的。但它rc.local
可以用于许多目的,而且你可以把一些事情放在其中,而这些事情并不依赖于其中其他事情的成功。
不过,我回答这个问题的主要原因是解释你可以做些什么来找出你的 rc.local 哪里出了问题。我强烈建议在你的 rc.local 脚本的开头写上类似这样的内容:
exec 2> /tmp/rc.local.log # send stderr from rc.local to a log file
exec 1>&2 # send stdout to the same log file
set -x # tell sh to display commands before execution
date # good practice to record when stuff happened
答案4
将第一行从:更改
#!/bin/sh -e
为
!/bin/sh -e