Okay, get yourself a can of Jolt Cola and sit down. This is going to take a while...
The if
statement in the Unix shell takes a command (any command), processes that command, and will execute the then clause of the if statement if that command returns an exit code of zero. If that command returns a non-zero value, it will execute the else clause if one exists.
For example:
if ls foo 2>&1 > /dev/null
then
echo "File 'foo' exists"
else
echo "No Foo For you!"
fi
The above will run the ls foo
command. The output will be dropped via the 2>&1 > /dev/null
, so you won't see the listing if foo
exists, and you won't get an error message if foo
doesn't exist. However, the ls
command will still return 0
if foo
exists and a 1
if it doesn't exist.
This allows you to do all sorts of neat stuff:
if mv foo foo.backup
then
echo "Renamed `foo` directory to `foo.backup`"
else
echo "BIG HORKING ERROR: Can't rename 'foo' to 'foo.backup'"
exit 2 #I need to stop my program. This is a big problem.
fi
In the above bit of code, I try to rename my directory foo
to foo.backup
before I put in a new foo
directory. Maybe directory foo
doesn't exist, so the mv
command fails. Maybe I don't have permission to rename the directory. Whatever it is, the if
statement allows me to test whether my mv
command succeeded or not.
This is all the if
command does: It executes a command, and depending whether the return value is zero or not, it may or may not execute the then clause.
But, what if I want to test whether a particular condition exists? For example:
- Is there a directory called
foo
?
- Is the value of my environment variable
$value
greater than 3?
Wouldn't it be nice if there was some sort of command which would allow me to test these things? Hey, if that command returns a zero value if my test is true and a non-zero value if my test if false, I could incorporate it into the if
statement!
Fortunately, there is a command in Unix called test that does exactly this:
if test -d foo
then
echo "Directory foo exists"
fi
if test $value -gt 3
then
echo "\$value is bigger than 3"
fi
It just also happens that the Unix command test
has a hard link to the [
command:
$ cd /bin
$ ls -li test [
54008404 -rwxr-xr-x 2 root wheel 18576 Jul 25 2012 [
54008404 -rwxr-xr-x 2 root wheel 18576 Jul 25 2012 test
The first column is the inode number. Since both are the same inode number, the [
command is simply a hard link to the test
command.
Now, things are a bit easier to understand. The [
1 is actually a Unix command! That's why I need a space between the square braces and the thing I'm testing:
# Invalid Syntax: I need spaces between the test and the braces:
if [$value -gt 3]
# Valid Syntax
if [ $value -gt 3 ]
# Actually the same command as above
if test value -gt 3
It also makes it much clearer why it's -gt
and -f
with a leading dash and not simply gt
or f
. That's because -gt
and -f
are parameters to the test command!
Now that you have a clearer understanding of how the built in shell if
works, and how the [
is actually a Unix command, we can look at your if statement and see what the problem is:
if [ "-f "/data/sb3???sh25t.sqfs" && "! -f /data/sb3??????sh25t.sqfs" && \
"! -f /data/sb3????????sh25t.sqfs" && "! -f /data/sb3???????sh25t.sqfs" ]
First, you have:
"-f /data/sb3???sh25t.sqfs"
The quotes make the whole string one single parameter to the test command which isn't what you want. You want the -f
parameter separate from the file you're testing. Thus, you need to remove the -f
from the quotes, so the test command sees it as a parameter:
if [ -f "/data/sb3???sh25t.sqfs" && ! -f "/data/sb3??????sh25t.sqfs" && \
! -f "/data/sb3????????sh25t.sqfs" && ! -f "/data/sb3???????sh25t.sqfs" ]
This will still not work. Take a look at the (man page)[http://www.manpagez.com/man/1/test/] for the test
command. Note that the &&
is not a valid parameter for the test
command. Instead, you're suppose to use -a
to combine two expressions_ your testing:
if [ -f "/data/sb3???sh25t.sqfs" -a ! -f "/data/sb3??????sh25t.sqfs" -a \
! -f "/data/sb3????????sh25t.sqfs" -a ! -f "/data/sb3???????sh25t.sqfs" ]
then
The -f
is outside of the quotes (and so are the !
), and I'm using -a
to and all four conditions together.
I could also do something like this:
if [ -f "/data/sb3???sh25t.sqfs" ] && [ ! -f "/data/sb3??????sh25t.sqfs" ] && \
! -f "/data/sb3????????sh25t.sqfs" ] && [ ! -f "/data/sb3???????sh25t.sqfs" ]
The &&
is actually a bash shell structure called a list operator. It links two separate command together, and does the following:
- Run the first command.
- If the first command returns a non-zero value, run the second command. Otherwise, return the exit code of the first command and don't run the second command.
For example, it's quite common to see something like this:
[ -d foo ] && rm -rf foo
or (the same thing):
test -d foo && rm -rf foo
In the above commands, I'm testing to see if foo
exists as a directory. If it does exist, my test command returns a true value, and I then execute the second command that removes the directory. It's the same as...
if [ -d foo ]
then
rm -rf foo
fi
Thus, my if statement consists of four separate commands. The first [...]
executes, and sees whether the file /data/sb3???sh25t.sqfs
actually exists. If it does, that test is true, returns a zero exit code. Then, the &&
executes the next command in the list.
The next test command sees if /data/sb3??????sh25t.sqfs
does not exists. If that file does not exist, the test command returns a zero exit code, and the next &&
list operator runs the next test command. Only if the first three test
commands are true, is the last test command is executed. If the last test command is also a true statement, then your then clause will be executed.
I hope you understand how that if
now works and why you were having your issues.
Sadly there is one more issue that's happening, and that's how the Unix shell works and how globbing interacts with the command line.
Unlike its counterparts in other operating systems, (cough Windows! cough), the Unix shell is very intelligent and also very meddlesome.
Try this command from the command line:
$ echo "Print an *" #Use quotes
Print an *
Now try this:
$ echo Print an * #No quotes
Print an bin foo bar...
This prints all "Print an" followed by all of the files in your current directory.
Try this:
$ touch foo.out foo.txt
$ echo "Do I have any foo.??? files"
Do I have any foo.??? files
$ echo Do I have any foo.??? files
Do I have any foo.out foo.txt files
Again, without the quotes, what I asked it to print out was changed. This is because the Unix shell is being meddlesome. What is happening is that the Unix shell sees the globbing pattern foo.???
and replaces that pattern with all matching files before the echo
command has a chance to echo anything.
You can see this happening with the following:
$ set -x #Turns on `xtrace`
$ touch foo.out foo.txt
+ touch foo.out foo.txt
$ echo "Do I have any foo.??? files"
+ echo "Do I have any foo.??? files"
Do I have any foo.??? files
$ echo Do I have any foo.??? files
+ echo Do I have any foo.out foo.txt files
Do I have any foo.out foo.txt files
$ set +x #Turns off `xtrace`
The xtrace
feature shows you what the command you're going to execute after the shell mucks around with your command. The lines that start with +
show you what your command will actually execute.
The reason I bring this up is due to a possible issue. If you happen to have more than one file that matches your pattern, all files that match your pattern will be substituted into your test command. Let's take a look at the command one more time:
if [ -f /data/sb3???sh25t.sqfs ] && [ ! -f /data/sb3??????sh25t.sqfs ] && \
[ ! -f /data/sb3????????sh25t.sqfs ] && [ ! -f /data/sb3???????sh25t.sqfs ]
If you have a file /data/sb3aaash25t.sqfs
and one called /data/sb3bbbsh25t.sqfs
, the first test command in your if statement will really look like this:
if [-f /data/sb3aaash25t.sqfs /data/sb3bbbsh25t.sqfs ]
which may not work since the -f
parameter only takes a single file name. Thus, even if you do everything mentioned above, you could still end up with a non-working program. Even worse it will fail intermittently which is even harder to debug.
One way around this is to forget about the test command, and use the ls
command like you saw in the very top part of this answer:
if ls /data/sb3???25t.sqfs 2>&1 > /dev/null
then
echo "At least one file that matches pattern sb3???25t.sqfs exists"
fi
Thus, you could do this:
if ls /data/sb3???sh25t.sqfs 2>&1 > /dev/null && \
! ls /data/sb3??????sh25t.sqfs 2>&1 > /dev/null && \
! ls /data/sb3????????sh25t.sqfs 2>&1 > /dev/null && \
! ls /data/sb3???????sh25t.sqfs 2>&1 > /dev/null
then
since ls
can take more than one file.
I'm sorry about the long explanation. 99% of the time, you could go on your merry way without needing to understand how the if
statement works, or that [...]
is really a completely separate Unix command from the if
, or how that meddlesome Unix shell will trip you up.
It just happened that you've struck the jackpot with this question. If you had played the Lotto today instead of writing your script, you could have told your boss what you really think of them, quite your job, and move to some tropical island paradise where the culture is so primitive, they have no word for terminal emulator.
Instead, it's back to the salt mines tomorrow.
1 Yes, yes. I know that [
is built into the BASH shell, and isn't the /bin/[
command. However, the shell built in [
acts like the /bin/[
command.