Differences between revisions 8 and 279 (spanning 271 versions)
Revision 8 as of 2005-08-28 22:44:10
Size: 9228
Editor: dsl-213-023-210-101
Comment:
Revision 279 as of 2009-01-30 18:19:10
Size: 189
Editor: 78
Comment: DiO3vd <a href="http://wzkbnzpilpvr.com/">wzkbnzpilpvr</a>, [url=http://bwdijetpmmsx.com/]bwdijetpmmsx[/url], [link=http://qjdgumxjoyhy.com/]qjdgumxjoyhy[/link], http://liysqqdwutqr.com/
Deletions are marked like this. Additions are marked like this.
Line 1: Line 1:
#pragma section-numbers 2

= Bash Pitfalls =

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

[[TableOfContents]]

== for i in `ls *.mp3` ==

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

 {{{
 for i in `ls *.mp3`; do # Wrong!
    some command $i # Wrong!
 done}}}

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:

 {{{
 for i in "`ls *.mp3`"; do # Wrong!
 ...}}}

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?

 {{{
 for i in *.mp3; do # Right!
   some command "$i"
 done}}}

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:Self:BashFaq#faq20 Bash FAQ #20].

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

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

 {{{
 mv "$file" "$target"}}}

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.

== [ $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.

 {{{
 [ "$foo" = bar ] # Right!}}}

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

 {{{
 [[ $foo = bar ]] # Also right!}}}

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.

== [ "$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:

 {{{
 [ "$foo" = bar -a "$bar" = foo ] # Right!
 [ "$foo" = bar ] && [ "$bar" = foo ] # Also right!
 [[ $foo = bar && $bar = foo ]] # Also right!}}}

== [[ $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:

 {{{
 ((foo > 7)) # Right!}}}

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:

 {{{
 [ $foo -gt 7 ] # Also right!}}}

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.

== 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:Self:BashFaq#faq24 Bash FAQ #24].

== 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:

 {{{
 if COMMANDS
 then
   COMMANDS
 elif COMMANDS # optional
 then
   COMMANDS
 else # optional
   COMMANDS
 fi}}}

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:

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

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.

== 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:

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

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.

== 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:

 {{{
 sed 's/foo/bar/g' file > tmpfile && mv tmpfile file}}}

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

 {{{
 sed -i 's/foo/bar/g' file(s)}}}

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

 {{{
 perl -pi -e 's/foo/bar/g' file}}}
DiO3vd <a href="http://wzkbnzpilpvr.com/">wzkbnzpilpvr</a>, [url=http://bwdijetpmmsx.com/]bwdijetpmmsx[/url], [link=http://qjdgumxjoyhy.com/]qjdgumxjoyhy[/link], http://liysqqdwutqr.com/

BashPitfalls (last edited 2024-10-05 08:59:29 by emanuele6)