3

I have following code

#!/bin/bash
 if [   "-f /data/sb3???sh25t.sqfs" && "! -f /data/sb3??????sh25t.sqfs"  && "! -f /data/sb3????????sh25t.sqfs" && "! -f /data/sb3???????sh25t.sqfs" ]  ;then
      select build in `ls -D /data/tftpboot/sb3???sh25t.sqfs` ; do
         break
      done
    build=${build:15}
 fi

When I run this script it gives me an error:

missing `]'

How can I resolve this?

Michael Petrotta
  • 59,888
  • 27
  • 145
  • 179
  • Can you perhaps describe in words what you're trying to do? We might be able to give a much better solution if we knew what it was supposed to do, rather than to try to fix how you're currently doing it. – that other guy Feb 14 '13 at 17:38
  • I have following files in my data folder and they have different lengths 1-ab393_ef25t.txt 2-ab393_v2_ef25t.txt 3-ab393_V2_ef25t.txt 4-ab393_eng_ef25t.txt 5-ab393_ENG_ef25t.txt 6-ab393_abcd_ef25t.txt 7-ab393_prod_ef25t.txt Here is the thing that i want to do: if [ -f 1 && ! -f 2 && ! -f 3 && ! -f 4] then list just file number 1 ( both lower case and upper cases) if [ -f 1 && -f 2 && ! -f 3 && ! -f 4 ] then list 1 and 2 only (both lower case and upper cases) – user2072910 Feb 14 '13 at 18:40

4 Answers4

5

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.

David W.
  • 105,218
  • 39
  • 216
  • 337
  • Found this question while browsing, stopped to read your amazing answer, and feel smarter for it, this is why SO rocks. :) Please please keep answering like this. – Techdragon Feb 13 '14 at 10:50
0

Use -a instead of && and don't add double quotes around expressions like "! -f /filename". Also note that -f /data/sb3????????sh25t.sqfs does not work if there are multiple files matching the pattern.

that other guy
  • 116,971
  • 11
  • 170
  • 194
0

Also you have && inside singe [ ] This is not allowed

Either use [[ and ]] or take the && out like this

[ -f file1 ] && [ -f file2 ]

Since you are using globs, you cannot use the syntax below:

But it is better to use

[[ -f file1 && -f file2 ]]

Note that the later only works in bash more info here:

Simple logical operators in Bash

Community
  • 1
  • 1
user000001
  • 32,226
  • 12
  • 81
  • 108
  • Note that he's using globs for the filenames, which aren't expanded in `[[ ]]` – that other guy Feb 14 '13 at 17:24
  • @thatotherguy Good point. But he can still use the first syntax with the `&&` outside the `[` `]` – user000001 Feb 14 '13 at 17:26
  • I have following files in my data folder and they have different lengths 1-ab393_ef25t.txt 2-ab393_v2_ef25t.txt 3-ab393_V2_ef25t.txt 4-ab393_eng_ef25t.txt 5-ab393_ENG_ef25t.txt 6-ab393_abcd_ef25t.txt 7-ab393_prod_ef25t.txt Here is the thing that i want to do: if [ -f 1 && ! -f 2 && ! -f 3 && ! -f 4] then list just file number 1 ( both lower case and upper cases) if [ -f 1 && -f 2 && ! -f 3 && ! -f 4 ] then list 1 and 2 only (both lower case and upper cases) – user2072910 Feb 14 '13 at 19:02
0

Your code is better written as

#!/bin/bash
if [   -f /data/sb3???sh25t.sqfs ] &&
   [ ! -f /data/sb3??????sh25t.sqfs ] &&
   [ ! -f /data/sb3???????sh25t.sqfs ] &&
   [ ! -f /data/sb3????????sh25t.sqfs ]; then
   select build in /data/tftpboot/sb3???sh25t.sqfs ; do
       break
   done
   build="${build:15}"
fi

(I reordered the f tests to make it easier to see the pattern in the file names you were testing.)

Don't quote the -f operators (but do be in the habit of quoting the operands) and put each test in a separate test/[ command. There's no need to run ls in your select command, as the pattern expansion already produces the list of files to choose from; ls would just take that list and reproduce it, so it's a no-op.

chepner
  • 497,756
  • 71
  • 530
  • 681
  • If `/data/sb3???sh25t.sqfs` is indeed a glob and not some form of code obfuscation, it won't expand in double quotes. – that other guy Feb 14 '13 at 17:34
  • Good point. It is likely a glob, given its use in the `select` statement. – chepner Feb 14 '13 at 17:36
  • I have following files in my data folder and they have different lengths 1-ab393_ef25t.txt 2-ab393_v2_ef25t.txt 3-ab393_V2_ef25t.txt 4-ab393_eng_ef25t.txt 5-ab393_ENG_ef25t.txt 6-ab393_abcd_ef25t.txt 7-ab393_prod_ef25t.txt Here is the thing that i want to do: if [ -f 1 && ! -f 2 && ! -f 3 && ! -f 4] then list just file number 1 ( both lower case and upper cases) if [ -f 1 && -f 2 && ! -f 3 && ! -f 4 ] then list 1 and 2 only (both lower case and upper cases) – user2072910 Feb 14 '13 at 19:03