20.2. Redirecting Code Blocks

Blocks of code, such as while, until, and for loops, even if/then test blocks can also incorporate redirection of stdin. Even a function may use this form of redirection (see Example 24-11). The < operator at the end of the code block accomplishes this.


Example 20-5. Redirected while loop

   1 #!/bin/bash
   2 # redir2.sh
   3 
   4 if [ -z "$1" ]
   5 then
   6   Filename=names.data       # Default, if no filename specified.
   7 else
   8   Filename=$1
   9 fi  
  10 #+ Filename=${1:-names.data}
  11 #  can replace the above test (parameter substitution).
  12 
  13 count=0
  14 
  15 echo
  16 
  17 while [ "$name" != Smith ]  # Why is variable $name in quotes?
  18 do
  19   read name                 # Reads from $Filename, rather than stdin.
  20   echo $name
  21   let "count += 1"
  22 done <"$Filename"           # Redirects stdin to file $Filename. 
  23 #    ^^^^^^^^^^^^
  24 
  25 echo; echo "$count names read"; echo
  26 
  27 exit 0
  28 
  29 #  Note that in some older shell scripting languages,
  30 #+ the redirected loop would run as a subshell.
  31 #  Therefore, $count would return 0, the initialized value outside the loop.
  32 #  Bash and ksh avoid starting a subshell *whenever possible*,
  33 #+ so that this script, for example, runs correctly.
  34 #  (Thanks to Heiner Steven for pointing this out.)
  35 
  36 #  However . . .
  37 #  Bash *can* sometimes start a subshell in a PIPED "while-read" loop,
  38 #+ as distinct from a REDIRECTED "while" loop.
  39 
  40 abc=hi
  41 echo -e "1\n2\n3" | while read l
  42      do abc="$l"
  43         echo $abc
  44      done
  45 echo $abc
  46 
  47 #  Thanks, Bruno de Oliveira Schneider, for demonstrating this
  48 #+ with the above snippet of code.
  49 #  And, thanks, Brian Onn, for correcting an annotation error.


Example 20-6. Alternate form of redirected while loop

   1 #!/bin/bash
   2 
   3 # This is an alternate form of the preceding script.
   4 
   5 #  Suggested by Heiner Steven
   6 #+ as a workaround in those situations when a redirect loop
   7 #+ runs as a subshell, and therefore variables inside the loop
   8 # +do not keep their values upon loop termination.
   9 
  10 
  11 if [ -z "$1" ]
  12 then
  13   Filename=names.data     # Default, if no filename specified.
  14 else
  15   Filename=$1
  16 fi  
  17 
  18 
  19 exec 3<&0                 # Save stdin to file descriptor 3.
  20 exec 0<"$Filename"        # Redirect standard input.
  21 
  22 count=0
  23 echo
  24 
  25 
  26 while [ "$name" != Smith ]
  27 do
  28   read name               # Reads from redirected stdin ($Filename).
  29   echo $name
  30   let "count += 1"
  31 done                      #  Loop reads from file $Filename
  32                           #+ because of line 20.
  33 
  34 #  The original version of this script terminated the "while" loop with
  35 #+      done <"$Filename" 
  36 #  Exercise:
  37 #  Why is this unnecessary?
  38 
  39 
  40 exec 0<&3                 # Restore old stdin.
  41 exec 3<&-                 # Close temporary fd 3.
  42 
  43 echo; echo "$count names read"; echo
  44 
  45 exit 0


Example 20-7. Redirected until loop

   1 #!/bin/bash
   2 # Same as previous example, but with "until" loop.
   3 
   4 if [ -z "$1" ]
   5 then
   6   Filename=names.data         # Default, if no filename specified.
   7 else
   8   Filename=$1
   9 fi  
  10 
  11 # while [ "$name" != Smith ]
  12 until [ "$name" = Smith ]     # Change  !=  to =.
  13 do
  14   read name                   # Reads from $Filename, rather than stdin.
  15   echo $name
  16 done <"$Filename"             # Redirects stdin to file $Filename. 
  17 #    ^^^^^^^^^^^^
  18 
  19 # Same results as with "while" loop in previous example.
  20 
  21 exit 0


Example 20-8. Redirected for loop

   1 #!/bin/bash
   2 
   3 if [ -z "$1" ]
   4 then
   5   Filename=names.data          # Default, if no filename specified.
   6 else
   7   Filename=$1
   8 fi  
   9 
  10 line_count=`wc $Filename | awk '{ print $1 }'`
  11 #           Number of lines in target file.
  12 #
  13 #  Very contrived and kludgy, nevertheless shows that
  14 #+ it's possible to redirect stdin within a "for" loop...
  15 #+ if you're clever enough.
  16 #
  17 # More concise is     line_count=$(wc -l < "$Filename")
  18 
  19 
  20 for name in `seq $line_count`  # Recall that "seq" prints sequence of numbers.
  21 # while [ "$name" != Smith ]   --   more complicated than a "while" loop   --
  22 do
  23   read name                    # Reads from $Filename, rather than stdin.
  24   echo $name
  25   if [ "$name" = Smith ]       # Need all this extra baggage here.
  26   then
  27     break
  28   fi  
  29 done <"$Filename"              # Redirects stdin to file $Filename. 
  30 #    ^^^^^^^^^^^^
  31 
  32 exit 0

We can modify the previous example to also redirect the output of the loop.


Example 20-9. Redirected for loop (both stdin and stdout redirected)

   1 #!/bin/bash
   2 
   3 if [ -z "$1" ]
   4 then
   5   Filename=names.data          # Default, if no filename specified.
   6 else
   7   Filename=$1
   8 fi  
   9 
  10 Savefile=$Filename.new         # Filename to save results in.
  11 FinalName=Jonah                # Name to terminate "read" on.
  12 
  13 line_count=`wc $Filename | awk '{ print $1 }'`  # Number of lines in target file.
  14 
  15 
  16 for name in `seq $line_count`
  17 do
  18   read name
  19   echo "$name"
  20   if [ "$name" = "$FinalName" ]
  21   then
  22     break
  23   fi  
  24 done < "$Filename" > "$Savefile"     # Redirects stdin to file $Filename,
  25 #    ^^^^^^^^^^^^^^^^^^^^^^^^^^^       and saves it to backup file.
  26 
  27 exit 0


Example 20-10. Redirected if/then test

   1 #!/bin/bash
   2 
   3 if [ -z "$1" ]
   4 then
   5   Filename=names.data   # Default, if no filename specified.
   6 else
   7   Filename=$1
   8 fi  
   9 
  10 TRUE=1
  11 
  12 if [ "$TRUE" ]          # if true    and   if :   also work.
  13 then
  14  read name
  15  echo $name
  16 fi <"$Filename"
  17 #  ^^^^^^^^^^^^
  18 
  19 # Reads only first line of file.
  20 # An "if/then" test has no way of iterating unless embedded in a loop.
  21 
  22 exit 0


Example 20-11. Data file names.data for above examples

   1 Aristotle
   2 Arrhenius
   3 Belisarius
   4 Capablanca
   5 Dickens
   6 Euler
   7 Goethe
   8 Hegel
   9 Jonah
  10 Laplace
  11 Maroczy
  12 Purcell
  13 Schmidt
  14 Schopenhauer
  15 Semmelweiss
  16 Smith
  17 Steinmetz
  18 Tukhashevsky
  19 Turing
  20 Venn
  21 Warshawski
  22 Znosko-Borowski
  23 
  24 #  This is a data file for
  25 #+ "redir2.sh", "redir3.sh", "redir4.sh", "redir4a.sh", "redir5.sh".

Redirecting the stdout of a code block has the effect of saving its output to a file. See Example 3-2.

Here documents are a special case of redirected code blocks. That being the case, it should be possible to feed the output of a here document into the stdin for a while loop.

   1 # This example by Albert Siersema
   2 # Used with permission (thanks!).
   3 
   4 function doesOutput()
   5  # Could be an external command too, of course.
   6  # Here we show you can use a function as well.
   7 {
   8   ls -al *.jpg | awk '{print $5,$9}'
   9 }
  10 
  11 
  12 nr=0          #  We want the while loop to be able to manipulate these and
  13 totalSize=0   #+ to be able to see the changes after the 'while' finished.
  14 
  15 while read fileSize fileName ; do
  16   echo "$fileName is $fileSize bytes"
  17   let nr++
  18   totalSize=$((totalSize+fileSize))   # Or: "let totalSize+=fileSize"
  19 done<<EOF
  20 $(doesOutput)
  21 EOF
  22 
  23 echo "$nr files totaling $totalSize bytes"