Advanced Bash-Scripting Guide: An in-depth exploration of the art of shell scripting | ||
---|---|---|
Prev | Chapter 19. I/O Redirection | Next |
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 23-11). The < operator at the end of the code block accomplishes this.
Example 19-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 19-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 19-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 19-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 19-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 19-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 19-11. Data file names.data for above examples
1 Aristotle 2 Belisarius 3 Capablanca 4 Euler 5 Goethe 6 Hamurabi 7 Jonah 8 Laplace 9 Maroczy 10 Purcell 11 Schmidt 12 Semmelweiss 13 Smith 14 Turing 15 Venn 16 Wilson 17 Znosko-Borowski 18 19 # This is a data file for 20 #+ "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" |