home | tech | misc | code | bookmarks (broken) | contact | README


sh notes

General notes about some idiosyncrasies of sh programming

After years making shell programming trying to sticky to Bourne shell, always, but making use of some extensions when they exist (like the local keyword for declaring local variables) I decided to write this part of this document with some notes, specially on some features that are not very good documented.

The shell I use to base my tests is the NetBSD /bin/sh.

Read a file or a output of a command line-by-line

A nice way to handle lines of a previous command is to pipe the command to while read, see:

m4 file.m4 | while read l; do
    do_something "$l"
done

But it is executed in a subshell, so trying to set variables inside it and use them later won't work. Take a look at this example:

last="(null)"
cat /etc/fstab | while read l; do
    last="$l"
done
echo "$last"

This example will print "(null)".

Also, all examples regarding while + read have problems. They will wrongly handle lines that start with space (chopping them off). Also, read handles backslashes in a special way. Take a look at your shell's man page and the -r flag of the read builtin. A very nice page about that and other limitations and security problems in shell is in Filenames and Pathnames in Shell: How to do it Correctly of David A. Wheeler Personal Home Page. We will take his advices when writing shell code.

If you want to use variables or any feature not possible when using a subshell, use the less elegant way:

last="(null)"
for l in $(cat /etc/fstab); do
    last="$l"
done
echo "$last"

But this will not work as we expect too, because "$()" (or evaluating use back quotes: ``) will treat not only newlines as separators but spaces and tabs as well.

A solution that works is to change the $IFS variable that holds what are the characters considered separators when expanding command output, variable, etc. In the following example I'm setting it to the newline character. See:

last="(null)"
old_IFS="$IFS"
IFS="
"
for l in $(cat /etc/fstab); do
    last="$l"
done
IFS="$old_IFS"
echo "$last"

It is interesting to read this and this pages for other discussions on this problem.

A much better approach seem to read the file from stdin, using a redirection:

last="(null)"
while read l; do
    last="$l"
done < /etc/fstab
echo "$last"

No part of this code will run in a subshell, so any variable change of the loop will kept to the outside part.

Call functions with `` or $() to get their output

It is common to call commands and local functions with `` or $() to store their output:

output="$(func)"

Although it is not a good pattern, because of shell programming limitations you might want to set a global variable inside your function. The problem is that functions inside `` and $() are executed inside a subshell. See this example:

var=no
func () {
        var=yes
        echo "output"
}
output="$(func)"
echo "$output"
echo "$var"

So, it is recommended that you look for another architecture.