07 September 2014

Many of those who are new to shell scripting often forget to quote strings or variables and hence embed bugs in their scripts. It makes things worse that these bugs are usually hard to reproduce though it is indeed very obvious by examining the scripts directly.

Quotes ensure strings recognized correctly

Without quotes, empty strings and strings with white-space cause problems.

[ "$VAR" == abc ]            # OK
[ $VAR == abc ]              # OOPS

# If VAR == ""
# ==> [ == abc ]
# ==> -bash: [: ==: unary operator expected

# If VAR == "a b c"
# ==> [ a b c = abc ]
# bash: [: too many arguments

Quotes preserve white-space in strings

# ls | grep -i linux              # OK
files=$(ls)
echo "$files" | grep -i linux   # OK
echo $files | grep -i linux     # OOPS, everything is in the same line

Quotes prevent shell expansions and substitutions

This is based on a real-world bug. A person spent days trying to reproduce a bug but just all in vein. In fact the script is rather short and simple. When that guy turned to me at last, I figured out the root cause instantly by skiming the script. Obviously, the poor guy should had used quotes in his script.

cat input | tr [A-Z] [a-z]      # OOPS

The issue here is that [A-Z] and [a-z] might be expanded by shell based on file name globing. Hence the result of this command line depends files under "current directory".

  • If there is no file named by a single alphabet character, the command line works as intended.
  • If there are some files of this pattern
    • You'll get an error message if you are lucky. You are lucky in the sense that you know there is something wrong. For instance, the actual command get run after shell expansion might be this:
      cat input | tr [A-Z] a b c # you are lucky as an error msg will
      
    • There are also chances that the command still runs but not the way you intended. This is a more dangerous situation since you are aware of the error. For example, the actual command run might be:
      show up ==> cat input | tr [A-Z] a # OOPS
      

      This is a valid command, hence there will be no error message. But this is not what you want.

The correct command is

cat input | tr '[A-Z]' '[a-z]'  # OK
cat input | tr 'A-Z' 'a-z'      # In fact, this is the correct usage of 'tr'

When running this kind of commands remotely, it is even trickier as both local file names and remote file names may impact the result.

ssh 135.1.2.3 'echo "[abcd]"' # OK
ssh 135.1.2.3 echo [abcd]     # WRONG
ssh 135.1.2.3 echo "[abcd]"   # WRONG
ssh 135.1.2.3 'echo [abcd]'   # WRONG

Think about why the later three one are wrong.

When to use 'eval'

#!/bin/bash

foo="hello    world"
bar="[$foo]"
echo "$bar"
# ==> [hello    world], this is expected

hello="echo $bar"
$hello                          # ==> [hello world], spaces squeezed

hello="echo '$bar'"
$hello                          # ==> '[hello world]', still not work

eval "$hello"                   # ==> [hello    world], yes!


blog comments powered by Disqus