Yesterday on the-list I was asking about some weird behavior about `test`, so it made me want to write about some other Bash scripting pitfalls which I have fallen into before. Maybe some of these you’ve run into before, maybe some you haven’t. Anyways, I hope you find something here helpful. Note that these apply to Bash. If you write scripts with other shells like zsh or ksh, these may or may not apply.
Reading and Writing to the Same File
You cannot read and write to the same file in the same command. One of two things will happen:
- The file will be clobbered to zero bytes.
- The file will grow until it consumes all available space.
This means you cannot write things like
$ cat file | some_command > file
You can’t do this because the pipe tells the shell to read from the file, but then `some_command` is writing out to it. Which takes precedence? There is not a reliable standard on this matter. Your only option is to create a temporary file.
$ cat file > /tmp/file && some_command /tmp/file
This will guarantee that the contents from the `cat` will be written out (flushed in C library terms) before the other command.
The Error Status of `cd`
Changing directories seems harmless, but you *must* check the return value of the `cd` command. Please always do this! Consider this in a script:
$ cd foo; rm *.php
Now what if `foo` does not exist? The `cd` will fail and you will delete all of the PHP scripts in a directory that you didn’t intend. Whoops. A way to avoid this is to explicitly check the results of `cd` by exiting the script if it fails, e.g.:
$ cd foo || exit 1; rm *.php
This `||` means ‘either change to the directory foo, or if you can’t, exit completely’. I really suggest doing this with every `cd`. If your script depends on changing directories, you do not want that to fail; God forbid you run a series of ‘dangerous’ commands in an unexpected directory.
Looping Over File Names
You may see this a lot online, as a way to loop over file names:
for script in $(ls *.php); do … done
for script in $(find . -type f -name '*.php'); do … done
Both of these forms will break in Bash for any file that has a space, because of word splitting rules. Let’s say you have a file called ‘user processor.php’. Both loops will execute twice, where the variable `$script` is equal to `user` and then `processor.php`. Which is not what you want in either case. The best thing to do is the same file glob:
for script in *.php; do ... done
This will do the right thing.