I have the following rc.local
script:
#!/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
The first line, startup_script.sh
actually downloads the script.sh
file to /script.sh
referred to in the third line.
Unfortunately, it appears that it is not making the script executeable or running the script. Running the rc.local
file manually after having started works perfectly. Can chmod not be run at startup or something?
You can skip all the way down to A Quick Fix but that's not necessarily the best option. So I recommend reading all this first.
rc.local
doesn't tolerate errors.rc.local
doesn't provide a way to intelligently recover from errors. If any command fails, it stops running. The first line,#!/bin/sh -e
, causes it to be executed in a shell invoked with the-e
flag. The-e
flag is what makes a script (in this case,rc.local
) stop running the first time a command fails within it.You want
rc.local
to behave like this. If a command fails, you do not want it to continue on with whatever other startup commands might be relying on it having succeeded.So if any command fails, the subsequent commands will not run. The problem here is that
/script.sh
did not run (not that it failed, see below), so most likely some command before it failed. But which one?Was it
/bin/chmod +x /script.sh
?No.
chmod
runs fine at any time. Provided the filesystem that contains/bin
was mounted, you can run/bin/chmod
. And/bin
is mounted beforerc.local
runs.When run as root,
/bin/chmod
rarely fails. It will fail if the file it operates on is read-only, and might fail if the filesystem it's on doesn't supported permissions. Neither is likely here.By the way,
sh -e
is the only reason it would actually be a problem ifchmod
failed. When you run a script file by explicitly invoking its interpreter, it doesn't matter if the file is marked executable. Only if it said/script.sh
would the file's executable bit matter. Since it sayssh /script.sh
, it doesn't (unless of course/script.sh
calls itself while it runs, which could fail from it not being executable, but it's unlikely it calls itself).So what failed?
sh /home/incero/startup_script.sh
failed. Almost certainly.We know it ran, because it downloaded
/script.sh
.(Otherwise, it would be important to make sure it did run, in case somehow
/bin
was not in PATH--rc.local
doesn't necessarily have the samePATH
as you have when you're logged in. If/bin
weren't inrc.local
's path, this would requiresh
to be run as/bin/sh
. Since it did run,/bin
is inPATH
, which means you can run other commands that are located in/bin
, without fully qualifying their names. For example, you can run justchmod
rather than/bin/chmod
. However, in keeping with your style inrc.local
, I've used fully qualified names for all commands exceptsh
, whenever I am suggesting you run them.)We can be pretty sure
/bin/chmod +x /script.sh
never ran (or you would see that/script.sh
was executed). And we knowsh /script.sh
wasn't run either.But it downloaded
/script.sh
. It succeeded! How could it fail?Two Meanings of Success
There are two different things a person might mean when s/he says a command succeeded:
And so it is for failure. When a person says a command failed, it could mean:
A script run with
sh -e
, likerc.local
, will stop running the first time a command reports that it failed. It doesn't make a difference what the command actually did.Unless you intend for
startup_script.sh
to report failure when it does what you want, this is a bug instartup_script.sh
.It's most likely that
startup_script.sh
did everything it should, except reported that it failed.How Success Or Failure Is Reported
A script is a list of zero or more commands. Each command has an exit status. Assuming there are no failures in actually running the script (for example, if the interpreter couldn't read the next line of the script while running it), the exit status of a script is:
0
(success) if the script was blank (i.e., had no commands).N
, if the script ended as a result of the commandexit N
, whereN
is some exit code.When an executable runs, it reports its own exit code--they're not just for scripts. (And technically, exit codes from scripts are the exit codes returned by the shells that run them.)
For example, if a C program ends with
exit(0);
, orreturn 0;
in itsmain()
function, the code0
is given to the operating system, which provides it to calling process (which may, for example, be the shell from which the program was run).0
means the program succeeded. Every other number means it failed. (This way, different numbers can sometimes refer to different reasons the program failed.)Commands Meant to Fail
Sometimes, you run a program with the intention that it will fail. In these situations, you might think of its failure as success, even though it is not a bug that the program reports failure. For example, you might use
rm
on a file you suspect already does not exist, just to make sure it's deleted.Something like this is probably happening in
startup_script.sh
, just before it stops running. The last command to run in the script is probably reporting failure (even though its "failure" might be totally fine or even necessary), which makes the script report failure.Tests Meant to Fail
One special kind of command is a test, by which I mean a command run for its return value rather than its side effects. That is, a test is a command that is run so that its exit status can be examined (and acted upon).
For example, suppose I forget if 4 is equal to 5. Fortunately, I know shell scripting:
Here, the test
[ -eq 5 ]
fails because it turns out 4 ≠ 5 after all. That doesn't mean the test didn't perform correctly; it did. It's job was to check if 4 = 5, then report success if so, and failure if not.You see, in shell scripting, success can also mean true, and failure can also mean false.
Even though the
echo
statement never runs, theif
block as a whole does return success.However, supposed I'd written it shorter:
This is common shorthand.
&&
is a boolean and operator. A&&
expression, which consists of&&
with statements on either side, returns false (failure) unless both sides return true (success). Just like a normal and.If someone asks you, "did Derek go to the mall and think about a butterfly?" and you know Derek didn't go to the mall, you don't have to bother figuring out if he thought of a butterfly.
Similarly, if the command to the left of
&&
fails (false), the whole&&
expression immediately fails (false). The statement on the right side of&&
is never run.Here,
[ 4 -eq 5 ]
runs. It "fails" (returning false). So the whole&&
expression fails.echo "Yeah, they're totally the same."
never runs. Everything behaved as it should be, but this command reports failure (even though the otherwise equivalentif
conditional above reports success).If that were the last statement in a script (and the script got to it, rather than terminating at some point before it), the whole script would report failure.
There's lots of tests besides this. For example, there are tests with
||
("or"). However, the above example should be sufficient to explain what tests are, and make it possible for you to effectively use documentation to determine if a particular statement/command is a test.sh
vs.sh -e
, RevisitedSince the
#!
line (see also this question) at the top of/etc/rc.local
hassh -e
, the operating system runs the script as though it were invoked with the command:In contrast, your other scripts, such as
startup_script.sh
, run without the-e
flag:Consequently they keep running even when a command in them reports failure.
This is normal and good.
rc.local
should be invoked withsh -e
and most other scripts--including most scripts run byrc.local
--should not.Just make sure to remember the difference:
Scripts run with
sh -e
exit reporting failure the first time a command they contain exits reporting failure.It's as though the script were a single long command consisting of all the commands in the script joined with
&&
operators.Scripts run with
sh
(without-e
) continue running until they get to a command that terminates (exits out of) them, or to the very end of the script. The success or failure of every command is essentially irrelevant (unless the next command checks it). The script exits with the exit status of the last command run.Helping Your Script Understand It's Not Such a Failure After All
How can you keep your script from thinking it failed when it didn't?
You look at what happens just before it's done running.
If a command failed when it ought to have succeeded, figure out why, and fix the problem.
If a command failed, and that was the right thing to happen, then prevent that failure status from propagating.
One way to keep a failure status from propagating is to run another command that succeeds.
/bin/true
has no side effects and reports success (just as/bin/false
does nothing and also fails).Another is to make sure the script is terminated by
exit 0
.That's not necessarily the same thing as
exit 0
being at the end of the script. For example, there might be anif
-block where the script exits inside.It's best to know what causes your script to report failure, before making it report success. If it really is failing in some way (in the sense of not doing what you want it to do), you don't really want it to report success.
A Quick Fix
If you can't make
startup_script.sh
exit reporting success, you can change the command inrc.local
that runs it, so that command reports success even thoughstartup_script.sh
did not.Currently you have:
This command has the same side effects (i.e., the side effect of running
startup_script.sh
), but always reports success:Remember, it's better to know why
startup_script.sh
reports failure, and fix it.How the Quick Fix Works
This is actually an example of a
||
test, an or test.Suppose you ask if I took out the trash or brushed the owl. If I took out the trash, I can truthfully say "yes," even if I don't remember whether or not I brushed the owl.
The command to the left of
||
runs. If it succeeds (true), then the right-hand side doesn't have to run. So ifstartup_script.sh
reports success, thetrue
command never runs.However, if
startup_script.sh
reports failure [I didn't take out the trash], then the result of/bin/true
[if I brushed the owl] matters./bin/true
always returns success (or true, as we sometimes call it). Consequently, the whole command succeeds, and the next command inrc.local
can run.An additional note on success/failure, true/false, zero/nonzero.
Feel free to ignore this. You might want to read this if you program in more than one language, though (i.e., not just shell scripting).
It is a point of great confusion for shell scripters taking up programming languages like C, and for C programmers taking up shell scripting, to discover:
In shell scripting:
0
means success and/or true.0
means failure and/or false.In C programming:
0
means false..0
means true.0
means success, other times it means failure, other times it means that the sum of the two numbers you just added is zero. Return values in general programming are used to signal a wide variety of different sorts of information.0
means false in your C program, you still make your program return0
as its exit code if you want it to report it succeeded (which the shell then interprets as true).You can (in Unix variants I use) override the default behavior by changing:
To:
At the start of the script. The "-e" flag instructs sh to exit on the first error. The long name of that flag is "errexit", so the original line is equivalent to:
@Bryce 's answer is perfectly fine in certain situations--depending on what you are using
rc.local
for. If it's absolutely critical that nothing happen after the first error, of course-e
is exactly what you want. Butrc.local
can be used for many purposes, and you could have things in there that are not dependent on other things in there having succeeded.Most of my reason for answering, though, is to explain what you can do to figure out where your rc.local went wrong. I highly recommend having something like this right at the beginning of your rc.local script:
change first line from:
#!/bin/sh -e
to!/bin/sh -e