Xiaopei's DokuWiki

These are the good times in your life,
so put on a smile and it'll be alright

User Tools

Site Tools


it:linux:shell_pitfalls

Shell Pitfalls

for i in $(ls *.mp3)

 for i in *.mp3; do
   [[ -f $i ]] || continue
   some command "$i"
 done

cp $file $target

文件名可能有空格, 所以应用双引号(“)扩起来.

 cp "$file" "$target"

Filenames with leading dashes(-)

ensure that your filenames always begin with a directory (including . for the current directory, if appropriate).

 for i in ./*.mp3; do
   cp "$i" /target
   ...

[ $foo = "bar" ]

You do not need to quote a string literal in bash (unless it contains metacharacters). But you should quote your variables if you aren't sure whether they could contain white space or wildcards.

[ bar = "$foo" ]

In bash, the [ [ keyword, which embraces and extends the old test command (also known as [), can also be used to solve the problem:

[[ $foo = bar ]]

You don't need to quote variable references on the left-hand side of = in [ [ ] ] because they don't undergo word splitting, and even blank variables will be handled correctly. On the other hand, quoting them won't hurt anything either.

cd $(dirname "$f")

You should quote it:

 cd "$(dirname "$f")"

What's not obvious here is how the quotes nest. Bash treats the double-quotes inside the command substitution as one pair, and the double-quotes outside the substitution as another pair.

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

You can't use && inside the old test (or [) command.

 [ bar = "$foo" ] && [ foo = "$bar" ] # Right!
 [[ $foo = bar && $bar = foo ]]       # Also right!

[[ $foo > 7 ]]

If you want to do a numeric comparison, you should use ( ( ) ) instead:

(($foo > 7))  # Right!
[[$foo -gt 7]] # Also Right!

grep foo bar | while read -r; do ((count++)); done

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).

if [grep foo myfile]

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

The syntax of an if statement is:

if COMMANDS
then
  COMMANDS
elif COMMANDS   # optional
then
  COMMANDS
else            # optional
  COMMANDS
fi              # required

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:

 if grep foo myfile >/dev/null; then
 ...
 fi

if [bar="$foo"]

 if [ bar = "$foo" ]

if [ [ a = b ] && [ c = d ] ]

 if [ a = b ] && [ c = d ]
 # OR
 if [[ a = b && c = d ]]

read $foo

 read foo
 # OR
 IFS= read -r foo

echo $foo

The only absolutely sure way to print the value of a variable is using printf:

 printf "%s\n" "$foo" 

$foo = bar

 foo= bar    # WRONG!
 foo =bar    # WRONG!
 $foo = bar; # COMPLETELY WRONG!
 
 foo=bar     # Right.
 foo="bar"   # More Right.

echo <<EOF

  # This is wrong:
  echo <<EOF
  Hello world
  How's it going?
  EOF
 
  # This is what you were trying to do:
  cat <<EOF
  Hello world
  How's it going?
  EOF
 
  cat <<EOF > /tmp/motd
  Hello world
  How's it going?
  EOF
 
  # Or, use quotes which can span multiple lines (efficient, echo is built-in):
  echo "Hello world
  How's it going?"

cd /foo; bar

You must always check for errors from a cd command. The simplest way to do that is:

 cd /foo && bar

If there's more than just one command after the cd, you might prefer this:

 cd /net || exit 1
 # cd /net || { echo "Can't read /net. Make sure you've logged in to the Samba network, and try again."; exit 1; }
 bar
 baz
 bat ... # Lots of commands.

By the way, if you're changing directories a lot in a Bash script, be sure to read the Bash help on pushd, popd, and dirs. Perhaps all that code you wrote to manage cd's and pwd's is completely unnecessary.

Speaking of which, compare this:

find ... -type d -print0 | while IFS= read -r -d '' subdir; do
  here=$PWD
  cd "$subdir" && whatever && ...
  cd "$here"
done

With this:

find ... -type d -print0 | while IFS= read -r -d '' subdir; do
  (cd "$subdir" || exit; whatever; ...)
done

Forcing a SubShell here causes the cd to occur only in the subshell.

[ bar == "$foo" ]

The == operator is not valid for the [ command. Use = or the [ [ keyword instead.

 [ bar = "$foo" ] && echo yes
 [[ bar == $foo ]] && echo yes

for i in {1..10}; do ./something &; done

You cannot put a ; immediately after an &. Just remove the extraneous ; entirely.

 for i in {1..10}; do ./something & done
 # OR
 for i in {1..10}; do
   ./something &
 done

& already functions as a command terminator, just like ; does. And you cannot mix the two.

cmd1 && cmd2 || cmd3

Some people like to use && and || as a shortcut syntax for if … then … else … fi. In many cases, this is perfectly safe. However, this construct is not completely equivalent to if … fi in the general case, because the command that comes after the && also generates an exit status. And if that exit status isn't “true” (0), then the command that comes after the || will also be invoked.

echo "Hello World!"

 set +H
 echo "Hello World!"

for arg in $*

 for arg in "$@"
 
 # Or simply:
 for arg

function foo()

for maximum portability, you should always use:

 foo() {
      ...
 }

echo "~"

Using $HOME rather than '~':

 "$HOME/dir with spaces" # expands to "/home/my photos/dir with spaces"

local varname=$(command)

 local varname
 varname=$(command)
 rc=$?

export foo=~/bar

 foo=~/bar; export foo    # Right!
 export foo="$HOME/bar"   # Right!

tr [A-Z] [a-z]

tr - translate or delete characters

There are (at least) three things wrong here. The first problem is that [A-Z] and [a-z] are seen as globs by the shell. If you don't have any single-lettered filenames in your current directory, it'll seem like the command is correct; but if you do, things will go wrong. Probably at 0300 hours on a weekend.

The second problem is that this is not really the correct notation for tr. What this actually does is translate '[' into '['; anything in the range A-Z into a-z; and ']' into ']'. So you don't even need those brackets, and the first problem goes away.

The third problem is that depending on the locale, A-Z or a-z may not give you the 26 ASCII characters you were expecting. In fact, in some locales z is in the middle of the alphabet! The solution to this depends on what you want to happen:

 # Use this if you want to change the case of the 26 latin letters
 LC_COLLATE=C tr A-Z a-z
 
 # Use this if you want the case conversion to depend upon the locale, which might be more like what a user is expecting
 tr '[:upper:]' '[:lower:]'
 
 # Example
 $echo 'Hello' | tr '[:upper:]' '[:lower:]'
 hello

The quotes are required on the second command, to avoid globbing.

ps ax | grep gedit

$ pgrep gedit

Unfortunately some programs aren't started with their name, for example firefox is often started as firefox-bin, which you would need to find out with - well - ps ax | grep firefox.

printf "$foo"

Always supply your own format string:

printf %s "$foo"
printf '%s\n' "$foo"

for i in {1..$n}

for ((i=1; i<=n; i++))

if [[ $foo = $bar ]] (depending on intent)

If bar contains *, the result will always be true. If you want to check for equality of strings, the right-hand side should be quoted:

 if [[ $foo = "$bar" ]]

If you want to do pattern matching, it might be wise to choose variable names that indicate the right-hand side contains a pattern. Or use comments.

It's also worth pointing out that if you quote the right-hand side of =~ it also forces a simple string comparison, rather than a regular expression matching.

if [[ $foo =~ 'some RE' ]]

The quotes around the right-hand side of the =~ operator cause it to become a string, rather than a RegularExpression. If you want to use a long or complicated regular expression and avoid lots of backslash escaping, put it in a variable:

$ foo=asdf
$ [[ $foo =~ '.+' ]] && echo 'yes' || echo 'no'
no
$ bar='.+'
$ [[ $foo =~ $bar ]] && echo 'yes' || echo 'no'
yes

[ -n $foo ] or [ -z $foo ]

$ man test
...
-n STRING
       the length of STRING is nonzero
 
STRING equivalent to -n STRING
 
-z STRING
       the length of STRING is zero

When using the [ command, you must quote each substitution that you give it. Otherwise, $foo could expand to 0 words, or 42 words, or any number of words that isn't 1, which breaks the syntax.

[ -n "$foo" ]
[ -z "$foo" ]
[ -n "$(some command with a "$file" in it)" ]
 
# [[ doesn't perform word-splitting or glob expansion, so you could also use:
[[ -n $foo ]]
[[ -z $foo ]]
[[ -e "$broken_symlink" || -L "$broken_symlink" ]]
it/linux/shell_pitfalls.txt · Last modified: 2013/08/19 07:22 (external edit)