Bash Pitfalls

This page shows common errors that Bash programmers make. The following examples are all flawed in some way:

TableOfContents

1. for i in `ls *.mp3`

One of the most common mistakes ["BASH"] programmers make is to write a loop like this:

This breaks when the user has a file with a space in its name. Why? Because the output of the ls *.mp3 command substitution undergoes word splitting. Assuming we have a file named 01 - Don't Eat the Yellow Snow.mp3 in the current directory, the for loop will iterate over each word in the resulting file name (namely: "01", "-", "Don't", "Eat", and so on).

You can't double-quote the substitution either:

This causes the entire output of the ls command to be treated as a single word, and instead of iterating over each file name in the output list, the loop will only execute once, with i taking on a value which is the concatenation of all the file names (with spaces between them).

In addition to this, the use of ls is just plain unnecessary. It's an external command, which simply isn't needed to do the job. So, what's the right way to do it?

Let Bash expand the list of filenames for you. The expansion will not be subject to word splitting. Each filename that's matched by the *.mp3 pattern will be treated as a separate word and, the loop will iterate once per file name.

For more details on this question, please see [wiki:BashFaq Bash FAQ #20].

The astute reader will notice the double quotes in the second line. This leads to our second common pitfall.

2. cp $file $target

What's wrong with the command shown above? Well, nothing, if you happen to know in advance that $file and $target have no white space in them.

But if you don't know that in advance, or if you're paranoid, or if you're just trying to develop good habits, then you should quote your variable references to avoid having them undergo word splitting.

Without the double quotes, you'll get a command like mv 01 - Don't Eat the Yellow Snow.mp3 /mnt/usb and then you'll get errors like mv: cannot stat `01': No such file or directory. With the double quotes, all's well, unless "$file" happens to start with a -, in which case mv thinks you're trying to feed it command line options. This isn't really a shell problem, but it often occurs with shell variables.

The solution is to insert -- between mv and its arguments. That tells it to stop scanning for options, and all is well:

3. [ $foo = "bar" ]

This is really the same as the previous pitfall, but I repeat it because it's so important. In the example above, the quotes are in the wrong place. You do not need to quote a string literal in bash. But you should quote your variables if you aren't sure whether they could contain white space.

Another way you could write this in bash involves the [[ keyword, which embraces and extends the old test command (also known as [).

You don't need to quote variable references within [[ ]] because they don't undergo word splitting in that context. On the other hand, quoting them won't hurt anything either.

4. [ "$foo" = bar && "$bar" = foo ]

You can't use && inside the old test (or [) command. The Bash parser sees && outside of [[ ]] or (( )) and breaks your command into two commands, before and after the &&. Use one of these instead:

5. [[ $foo > 7 ]]

The [[ ]] operator is not used for an ArithmeticExpression. It's used for strings only. If you want to do a numeric comparison against the constant 7, you must use (( )) instead:

If you use the > operator inside [[ ]], it's treated as a string comparison, not an integer comparison. This may work sometimes, but it will fail when you least expect it. If you use > inside [ ], it's even worse: it's an output redirection. You'll get a file named 7 in your directory, and the test will succeed as long as $foo is not empty.

If you're developing for a BourneShell instead of bash, this is the historically correct version:

Note that the test ... -gt command will fail in interesting ways if $foo is not an integer. Therefore, there's not much point in quoting it properly -- if it's got white space, or is empty, or is anything other than an integer, we're probably going to crash anyway. You'll need to sanitize your input aggressively.

6. grep foo bar | while read line; do ((count++)); done

The code above looks OK at first glance, doesn't it? Sure, it's just a poor implementation of grep -c, but it's intended as a simplistic example. So why doesn't it work? The variable count will be unchanged after the loop terminates, much to the surprise of Bash developers everywhere.

The reason this code does not work as expected is because each command in a pipeline is executed in a separate subshell. The changes to the count variable within the loop's subshell aren't reflected within the parent shell (the script in which the code occurs).

For solutions to this, please see [wiki:BashFaq Bash FAQ #24].

7. if [grep foo myfile]

Many people are confused by the common practice of putting the [ command after an if. They see this and convince themselves that the [ is part of the if statement's syntax, just like parentheses are used in C's if statement.

However, that is not the case! [ is a command, not a syntax marker for the if statement. It's equivalent to the test command, except for the requirement that the final argument must be a ].

The syntax of the if statement is as follows:

There may be zero or more optional elif sections, and one optional else section. Note: there is no [ in the syntax!

Once again, [ is a command. It takes arguments, and it produces an exit code. It may produce error messages. It does not, however, produce any standard output.

The if statement evaluates the first set of COMMANDS that are given to it (up until then, as the first word of a new command). The exit code of the last command from that set determines whether the if statement will execute the COMMANDS that are in the then section, or move on.

If you want to make a decision based on the output of a grep command, you do not need to enclose it in parentheses, brackets, backticks, or any other syntax mark-up! Just use grep as the COMMANDS after the if, like this:

Note that we discard the standard output of the grep (which would normally include the matching line, if any), because we don't want to see it -- we just want to know whether it's there. If the grep matches a line from myfile, then the exit code will be 0 (true), and the then clause will be executed. Otherwise, if there is no matching line, the grep should return a non-zero exit code.

8. if ["$foo"=bar]

As with the previous example, [ is a command. Just like with any other command, Bash expects the command to be followed by a space, then the first argument, then another space, etc. You can't just run things all together without putting the spaces in! Here is the correct way:

Each of "$foo" (after substitution, but without word splitting), =, bar and ] is a separate argument to the [ command. There must be spaces before each argument.

9. cat file | sed s/foo/bar/ > file

You cannot read from a file and write to it in the same pipeline. Depending on what your pipeline does, the file will either be clobbered (to 0 bytes, or possibly to a number of bytes equal to the size of your operating system's pipeline buffer), or the file will grow until it fills the available disk space or reaches your operating system's file size limitation.

If you want to make a change to a file, other than appending to the end of it, there must be a temporary file created at some point. For example, the following is completely portable:

The following will only work on GNU sed 4.x:

And the following requires perl 5 (which is probably more widely available than GNU sed 4.x):

For more details, please see [wiki:BashFaq Bash FAQ #21].

10. echo $foo

This relatively innocent-looking command causes massive confusion. Because the $foo isn't quoted, it will not only be subject to word splitting, but also file globbing. This misleads Bash programmers into thinking their variables contain the wrong values, when in fact the variables are OK -- it's just the echo that's messing up their view of what's happening.

This message is split into words and any globs are expanded, such as the *.zip. What will your users think when they see this message:

To demonstrate:

11. $foo=bar

No, you don't assign a variable by putting a $ in front of the variable name. This isn't perl.

12. foo = bar

No, you can't put spaces around the = when assigning to a variable. This isn't C. When you write foo = bar the shell splits it into three words. The first word, foo, is taken as the command name. The second and third become the arguments to that command.

Likewise, the following are also wrong:

  foo= bar    # WRONG!
  foo =bar    # WRONG!
  $foo = bar; # COMPLETELY WRONG!

  foo=bar     # Right.

13. [ $i = foo ] # "unary operator expected" if "i" is unset

If a variable reference in [ does not exist, or is blank, then the [ command normally sees the line:

[ $i = foo ]

...as:

[ = foo ]

...and throws the error "unary operator expected". As noted elsewhere, unquoted references to a variable inside of [ are a bad idea anyway, since you don't know if $i contains spaces. You may have seen code like this:

[ x$i = xfoo ]

...to deal with the problem, but it's better to add quotes, or use [[ instead of [:

unset i
[ $i = foo ] && echo true || echo false # <--- Throws an error.
[ "$i" = foo ] && echo true || echo false # <--- Prints "false", as expected.
[[ $i = foo ]] && echo true || echo false # <--- Prints "false", as expected.

14. echo <<EOF

A here document is a useful tool for embedding large blocks of textual data in a script. It causes a redirection of the lines of text in the script to the standard input of a command. Unfortunately, echo is not a command which reads from stdin.

  # This is wrong:
  echo <<EOF
  Hello world
  EOF

  # This is right:
  cat <<EOF
  Hello world
  EOF

15. su -c 'some command'

This syntax is almost correct. The problem is, su takes a -c argument, but it's not the one you want. You want to pass -c 'some command' to a shell, which means you need a username before the -c.

  su root -c 'some command'   # Now it's right.

su assumes a username of root when you omit one, but this falls on its face when you want to pass a command to the shell afterward. You must supply the username in this case.