How to raise and take care of shell scripts: subshells

When and how subshells and "children" are created?

Continuously ;-) Is that bad? No, it isn't - that's the way an operating system works. But wasting resources and making performance worse by creating unnecessary processes is bad. Some hints to avoid this you can find in the Script tuning chapter.
Here I want to show how new processes are created in your scripts, when they are subshells (some consequences are described in the Variables chapter) and how you can avoid it if you want.
Children of your script process are created:

if you start an external program
A shell builtin does not create a new process. To find out, whether a command you use is a shell builtin or not, read the Find chapter. Other shell scripts, which you start, are external programs too, except you start them in the way described below.

if you use a pipe
Every command on the right side of a pipe is executed in a subshell - this mainly means, that you can't modify variable values here - of course you can do it, but the modification takes no effect outside when the piped command has exited. Even other changes take no effect. Try this:

  jan@jack:~/tmp/subshell> cat chdir.sh
  #! /bin/bash
  cd subdir
  pwd
  jan@jack:~/tmp/subshell> ./chdir.sh
  /home/jan/tmp/subshell/subdir
  jan@jack:~/tmp/subshell> pwd
  /home/jan/tmp/subshell

That's not working, isn't it? What's the solution? There are a number of possible ways. The next example demonstrates, how to pass a variable value calculated in a subshell to the calling process. It's done easy: Write the variable's value to standard output (stdout) and catch it using command substitution (take care of the quoting - you may receive a list of file names):

  # search all files in a directory tree containing the text "hello"
  HELLO_FILES="`find . -type f -print | xargs grep -l 'hello'`"
  # find the first directory with a name starting with "hello" and
  # containing a file name "hello", then change into this directory
  NEW_DIR="`find . -type d -name 'hello*' -print | while read dir; do
              if test -f \"$dir/hello\"; then
                echo \"$dir\"
                break
              fi
            done`"
  test -n "$NEW_DIR" && cd "$NEW_DIR"

It should be possible to implement the second sample using a combination of find options, I wanted to demonstrate the general way how to use it.

Another, often used possibility is to source scripts. It's a common way to load configuration files. When sourcing files, they are executed within the current shell.

  # first sample: change into a directory (chdir.sh is the same as showed above)
  jan@jack:~/tmp/subshell> . ./chdir.sh
  /home/jan/tmp/subshell/subdir
  jan@jack:~/tmp/subshell/subdir> pwd
  /home/jan/tmp/subshell/subdir
  # second sample: load a configuration file
  jan@jack:~/tmp> cat config.cfg
  MY_VAR1=hello
  MY_VAR2=world
  jan@jack:~/tmp> source ./config.cfg
  jan@jack:~/tmp> echo "$MY_VAR1 $MY_VAR2"
  hello world

Now I have my own hello world example ;-) You see: the point is only another syntax for the source command. When sourcing scripts you have to take care on an important detail: If the script you want to source ends with exit, your current shell will exit too!

Here's another situation, where you have to pay attention to the subshell's behaviour following a pipe:

  # function copying and possibly renaming files
  # parameter 1: source directory
  # parameter 2: destination directory
  # parameter 3: suffix of files to copy
  # parameter 4: suffix for temporary files (optional)
  # exit code: 0 = o.k.
  #            1 = error
  function copy_and_rename_files {
    # search for files
    find "$1" -name "*.$3" -type f -print | while read fname; do

      # destination file name
      dstfile="$2/`basename \"$fname\"`"
      # destination file name for renaming
      mv_file=

      # if temp file suffix is given: append to destination name, set $mv_file
      if test -n "$4"; then
        mv_file="$dstfile"
        dstfile="${dstfile}.$4"
      fi

      # copy, exit loop in case of error
      if ! cp "$fname" "$dstfile" 2>dev/null; then
        # error message to stderr
        echo "error copying '$fname'" >&2
        return 1 # Attention! Exits only the subshell (loop), not the function!
      fi

      # if temp suffix is set: rename now
      # we assume that this will work, if the copy succeeded
      test -n "$mv_file" && mv "$dstfile" "$mv_file"

      # another trap: If no renaming is requested, the result of the loop
      # is the exit code of the test command in the last round, this is false (1)
      # to prevent from exiting the loop with "false" we add a dummy command,
      # which always returns 0 (true)
      true
    done

    # now we have to evaluate the loop's exit code
    return $?
  }

The last example was more detailed - I myself was running into the trap some times ago.