LDP/LDP/guide/docbook/Bash-Beginners-Guide/chap9.xml

591 lines
30 KiB
XML

<chapter id="chap_09">
<title>Repetitive tasks</title>
<abstract>
<para>Upon completion of this chapter, you will be able to</para>
<para>
<itemizedlist>
<listitem><para>Use <command>for</command>, <command>while</command> and <command>until</command> loops, and decide which loop fits which occasion.</para></listitem>
<listitem><para>Use the <command>break</command> and <command>continue</command> Bash built-ins.</para></listitem>
<listitem><para>Write scripts using the <command>select</command> statement.</para></listitem>
<listitem><para>Write scripts that take a variable number of arguments.</para></listitem>
</itemizedlist></para>
</abstract>
<sect1 id="sect_09_01"><title>The for loop</title>
<sect2 id="sect_09_01_01"><title>How does it work?</title>
<para>The <command>for</command> loop is the first of the three shell looping constructs. This loop allows for specification of a list of values. A list of commands is executed for each value in the list.</para>
<para>The syntax for this loop is:</para>
<cmdsynopsis><command>for <varname>NAME</varname> [in LIST ]; do COMMANDS; done</command></cmdsynopsis>
<para>If <command>[in LIST]</command> is not present, it is replaced with <command>in <varname>$@</varname></command> and <command>for</command> executes the <command>COMMANDS</command> once for each positional parameter that is set (see <xref linkend="sect_03_02_05" /> and <xref linkend="sect_07_02_01_02" />).</para>
<para>The return status is the exit status of the last command that executes. If no commands are executed because <varname>LIST</varname> does not expand to any items, the return status is zero.</para>
<para><varname>NAME</varname> can be any variable name, although <varname>i</varname> is used very often. <varname>LIST</varname> can be any list of words, strings or numbers, which can be literal or generated by any command. The <command>COMMANDS</command> to execute can also be any operating system commands, script, program or shell statement. The first time through the loop, <varname>NAME</varname> is set to the first item in <varname>LIST</varname>. The second time, its value is set to the second item in the list, and so on. The loop terminates when <varname>NAME</varname> has taken on each of the values from <varname>LIST</varname> and no items are left in <varname>LIST</varname>.</para>
</sect2>
<sect2 id="sect_09_01_02"><title>Examples</title>
<sect3 id="sect_09_01_02_03"><title>Using command substitution for specifying LIST items</title>
<para>The first is a command line example, demonstrating the use of a <command>for</command> loop that makes a backup copy of each <filename>.xml</filename> file. After issuing the command, it is safe to start working on your sources:</para>
<screen>
<prompt>[carol@octarine ~/articles]</prompt> <command>ls <filename>*.xml</filename></command>
file1.xml file2.xml file3.xml
<prompt>[carol@octarine ~/articles]</prompt> <command>ls <filename>*.xml</filename> &gt; <filename>list</filename></command>
<prompt>[carol@octarine ~/articles]</prompt> <command>for <varname>i</varname> in <parameter>`cat list`</parameter>; do cp <filename>"$i" "$i".bak</filename> ; done</command>
<prompt>[carol@octarine ~/articles]</prompt> <command>ls <filename>*.xml*</filename></command>
file1.xml file1.xml.bak file2.xml file2.xml.bak file3.xml file3.xml.bak
</screen>
<para>This one lists the files in <filename>/sbin</filename> that are just plain text files, and possibly scripts:</para>
<screen>
<command>for <varname>i</varname> in <parameter>`ls /sbin`</parameter>; do file <filename>/sbin/$i</filename> | grep <parameter>ASCII</parameter>; done</command>
</screen>
</sect3>
<sect3 id="sect_09_01_02_02"><title>Using the content of a variable to specify LIST items</title>
<para>The following is a specific application script for converting HTML files, compliant with a certain scheme, to PHP files. The conversion is done by taking out the first 25 and the last 21 lines, replacing these with two PHP tags that provide header and footer lines:</para>
<screen>
<prompt>[carol@octarine ~/html]</prompt> <command>cat <filename>html2php.sh</filename></command>
#!/bin/bash
# specific conversion script for my html files to php
LIST="$(ls *.html)"
for i in "$LIST"; do
NEWNAME=$(ls "$i" | sed -e 's/html/php/')
cat beginfile &gt; "$NEWNAME"
cat "$i" | sed -e '1,25d' | tac | sed -e '1,21d'| tac &gt;&gt; "$NEWNAME"
cat endfile &gt;&gt; "$NEWNAME"
done
</screen>
<para>Since we don't do a line count here, there is no way of knowing the line number from which to start deleting lines until reaching the end. The problem is solved using <command>tac</command>, which reverses the lines in a file.</para>
<tip><title>The basename command</title>
<para>Instead of using <command>sed</command> to replace the <filename>html</filename> suffix with <filename>php</filename>, it would be cleaner to use the <command>basename</command> command. Read the man page for more info.</para>
</tip>
<warning><title>Odd characters</title>
<para>You will run into problems if the list expands to file names containing spaces and other irregular characters. A more ideal construct to obtain the list would be to use the shell's globbing feature, like this:</para>
<screen>
for i in $PATHNAME/*; do
commands
done
</screen>
</warning>
</sect3>
</sect2>
</sect1>
<sect1 id="sect_09_02"><title>The while loop</title>
<sect2 id="sect_09_02_01"><title>What is it?</title>
<para>The <command>while</command> construct allows for repetitive execution of a list of commands, as long as the command controlling the <command>while</command> loop executes successfully (exit status of zero). The syntax is:</para>
<cmdsynopsis><command>while CONTROL-COMMAND; do CONSEQUENT-COMMANDS; done</command></cmdsynopsis>
<para><command>CONTROL-COMMAND</command> can be any command(s) that can exit with a success or failure status. The <command>CONSEQUENT-COMMANDS</command> can be any program, script or shell construct.</para>
<para>As soon as the <command>CONTROL-COMMAND</command> fails, the loop exits. In a script, the command following the <command>done</command> statement is executed.</para>
<para>The return status is the exit status of the last <command>CONSEQUENT-COMMANDS</command> command, or zero if none was executed.</para>
</sect2>
<sect2 id="sect_09_02_02"><title>Examples</title>
<sect3 id="sect_09_02_02_01"><title>Simple example using while</title>
<para>Here is an example for the impatient:</para>
<screen>
#!/bin/bash
# This script opens 4 terminal windows.
i="0"
while [ $i -lt 4 ]
do
xterm &amp;
i=$[$i+1]
done
</screen>
</sect3>
<sect3 id="sect_09_02_02_02"><title>Nested while loops</title>
<para>The example below was written to copy pictures that are made with a webcam to a web directory. Every five minutes a picture is taken. Every hour, a new directory is created, holding the images for that hour. Every day, a new directory is created containing 24 subdirectories. The script runs in the background.</para>
<screen>
#!/bin/bash
# This script copies files from my homedirectory into the webserver directory.
# (use scp and SSH keys for a remote directory)
# A new directory is created every hour.
PICSDIR=/home/carol/pics
WEBDIR=/var/www/carol/webcam
while true; do
DATE=`date +%Y%m%d`
HOUR=`date +%H`
mkdir $WEBDIR/"$DATE"
while [ $HOUR -ne "00" ]; do
DESTDIR=$WEBDIR/"$DATE"/"$HOUR"
mkdir "$DESTDIR"
mv $PICDIR/*.jpg "$DESTDIR"/
sleep 3600
HOUR=`date +%H`
done
done
</screen>
<para>Note the use of the <command>true</command> statement. This means: continue execution until we are forcibly interrupted (with <command>kill</command> or <keycap>Ctrl</keycap>+<keycap>C</keycap>).</para>
<para>This small script can be used for simulation testing; it generates files:</para>
<screen>
#!/bin/bash
# This generates a file every 5 minutes
while true; do
touch pic-`date +%s`.jpg
sleep 300
done
</screen>
<para>Note the use of the <command>date</command> command to generate all kinds of file and directory names. See the man page for more.</para>
<note><title>Use the system</title>
<para>The previous example is for the sake of demonstration. Regular checks can easily be achieved using the system's <emphasis>cron</emphasis> facility. Do not forget to redirect output and errors when using scripts that are executed from your crontab!</para>
</note>
</sect3>
<sect3 id="sect_09_02_02_03"><title>Using keyboard input to control the while loop</title>
<para>This script can be interrupted by the user when a <keycap>Ctrl</keycap>+<keycap>C</keycap> sequence is entered:</para>
<screen>
#!/bin/bash
# This script provides wisdom
FORTUNE=/usr/games/fortune
while true; do
echo "On which topic do you want advice?"
cat &lt;&lt; topics
politics
startrek
kernelnewbies
sports
bofh-excuses
magic
love
literature
drugs
education
topics
echo
echo -n "Make your choice: "
read topic
echo
echo "Free advice on the topic of $topic: "
echo
$FORTUNE $topic
echo
done
</screen>
<para>A <emphasis>here</emphasis> document is used to present the user with possible choices. And again, the <command>true</command> test repeats the commands from the <command>CONSEQUENT-COMMANDS</command> list over and over again.</para>
</sect3>
<sect3 id="sect_09_02_02_04"><title>Calculating an average</title>
<para>This script calculates the average of user input, which is tested before it is processed: if input is not within range, a message is printed. If <keycap>q</keycap> is pressed, the loop exits:</para>
<screen>
#!/bin/bash
# Calculate the average of a series of numbers.
SCORE="0"
AVERAGE="0"
SUM="0"
NUM="0"
while true; do
echo -n "Enter your score [0-100%] ('q' for quit): "; read SCORE;
if (("$SCORE" &lt; "0")) || (("$SCORE" &gt; "100")); then
echo "Be serious. Common, try again: "
elif [ "$SCORE" == "q" ]; then
echo "Average rating: $AVERAGE%."
break
else
SUM=$[$SUM + $SCORE]
NUM=$[$NUM + 1]
AVERAGE=$[$SUM / $NUM]
fi
done
echo "Exiting."
</screen>
<para>Note how the variables in the last lines are left unquoted in order to do arithmetic.</para>
</sect3>
</sect2>
</sect1>
<sect1 id="sect_09_03"><title>The until loop</title>
<sect2 id="sect_09_03_01"><title>What is it?</title>
<para>The <command>until</command> loop is very similar to the <command>while</command> loop, except that the loop executes until the <command>TEST-COMMAND</command> executes successfully. As long as this command fails, the loop continues. The syntax is the same as for the <command>while</command> loop:</para>
<cmdsynopsis><command>until TEST-COMMAND; do CONSEQUENT-COMMANDS; done</command></cmdsynopsis>
<para>The return status is the exit status of the last command executed in the <command>CONSEQUENT-COMMANDS</command> list, or zero if none was executed. <command>TEST-COMMAND</command> can, again, be any command that can exit with a success or failure status, and <command>CONSEQUENT-COMMANDS</command> can be any UNIX command, script or shell construct.</para>
<para>As we already explained previously, the <quote>;</quote> may be replaced with one or more newlines wherever it appears.</para>
</sect2>
<sect2 id="sect_09_03_02"><title>Example</title>
<para>An improved <filename>picturesort.sh</filename> script (see <xref linkend="sect_09_02_02_02" />), which tests for available disk space. If not enough disk space is available, remove pictures from the previous months:</para>
<screen>
#!/bin/bash
# This script copies files from my homedirectory into the webserver directory.
# A new directory is created every hour.
# If the pics are taking up too much space, the oldest are removed.
while true; do
DISKFUL=$(df -h $WEBDIR | grep -v File | awk '{print $5 }' | cut -d "%" -f1 -)
until [ $DISKFUL -ge "90" ]; do
DATE=`date +%Y%m%d`
HOUR=`date +%H`
mkdir $WEBDIR/"$DATE"
while [ $HOUR -ne "00" ]; do
DESTDIR=$WEBDIR/"$DATE"/"$HOUR"
mkdir "$DESTDIR"
mv $PICDIR/*.jpg "$DESTDIR"/
sleep 3600
HOUR=`date +%H`
done
DISKFULL=$(df -h $WEBDIR | grep -v File | awk '{ print $5 }' | cut -d "%" -f1 -)
done
TOREMOVE=$(find $WEBDIR -type d -a -mtime +30)
for i in $TOREMOVE; do
rm -rf "$i";
done
done
</screen>
<para>Note the initialization of the <varname>HOUR</varname> and <varname>DISKFULL</varname> variables and the use of options with <command>ls</command> and <command>date</command> in order to obtain a correct listing for <varname>TOREMOVE</varname>.</para>
</sect2>
</sect1>
<sect1 id="sect_09_04"><title>I/O redirection and loops</title>
<sect2 id="sect_09_04_01"><title>Input redirection</title>
<para>Instead of controlling a loop by testing the result of a command or by user input, you can specify a file from which to read input that controls the loop. In such cases, <command>read</command> is often the controlling command. As long as input lines are fed into the loop, execution of the loop commands continues. As soon as all the input lines are read the loop exits.</para>
<para>Since the loop construct is considered to be one command structure (such as <command>while TEST-COMMAND; do CONSEQUENT-COMMANDS; done</command>), the redirection should occur after the <command>done</command> statement, so that it complies with the form</para>
<cmdsynopsis><command>command &lt; <filename>file</filename></command></cmdsynopsis>
<para>This kind of redirection also works with other kinds of loops.</para>
</sect2>
<sect2 id="sect_09_04_02"><title>Output redirection</title>
<para>In the example below, output of the <command>find</command> command is used as input for the <command>read</command> command controlling a <command>while</command> loop:</para>
<screen>
<prompt>[carol@octarine ~/testdir]</prompt> <command>cat <filename>archiveoldstuff.sh</filename></command>
#!/bin/bash
# This script creates a subdirectory in the current directory, to which old
# files are moved.
# Might be something for cron (if slightly adapted) to execute weekly or
# monthly.
ARCHIVENR=`date +%Y%m%d`
DESTDIR="$PWD/archive-$ARCHIVENR"
mkdir "$DESTDIR"
# using quotes to catch file names containing spaces, using read -d for more
# fool-proof usage:
find "$PWD" -type f -a -mtime +5 | while read -d $'\000' file
do
gzip "$file"; mv "$file".gz "$DESTDIR"
echo "$file archived"
done
</screen>
<para>Files are compressed before they are moved into the archive directory.</para>
</sect2>
</sect1>
<sect1 id="sect_09_05"><title>Break and continue</title>
<sect2 id="sect_09_05_01"><title>The break built-in</title>
<para>The <command>break</command> statement is used to exit the current loop before its normal ending. This is done when you don't know in advance how many times the loop will have to execute, for instance because it is dependent on user input.</para>
<para>The example below demonstrates a <command>while</command> loop that can be interrupted. This is a slightly improved version of the <filename>wisdom.sh</filename> script from <xref linkend="sect_09_02_02_03" />.</para>
<screen>
#!/bin/bash
# This script provides wisdom
# You can now exit in a decent way.
FORTUNE=/usr/games/fortune
while true; do
echo "On which topic do you want advice?"
echo "1. politics"
echo "2. startrek"
echo "3. kernelnewbies"
echo "4. sports"
echo "5. bofh-excuses"
echo "6. magic"
echo "7. love"
echo "8. literature"
echo "9. drugs"
echo "10. education"
echo
echo -n "Enter your choice, or 0 for exit: "
read choice
echo
case $choice in
1)
$FORTUNE politics
;;
2)
$FORTUNE startrek
;;
3)
$FORTUNE kernelnewbies
;;
4)
echo "Sports are a waste of time, energy and money."
echo "Go back to your keyboard."
echo -e "\t\t\t\t -- \"Unhealthy is my middle name\" Soggie."
;;
5)
$FORTUNE bofh-excuses
;;
6)
$FORTUNE magic
;;
7)
$FORTUNE love
;;
8)
$FORTUNE literature
;;
9)
$FORTUNE drugs
;;
10)
$FORTUNE education
;;
0)
echo "OK, see you!"
break
;;
*)
echo "That is not a valid choice, try a number from 0 to 10."
;;
esac
done
</screen>
<para>Mind that <command>break</command> exits the loop, not the script. This can be demonstrated by adding an <command>echo</command> command at the end of the script. This <command>echo</command> will also be executed upon input that causes <command>break</command> to be executed (when the user types <quote>0</quote>).</para>
<para>In nested loops, <command>break</command> allows for specification of which loop to exit. See the Bash <command>info</command> pages for more.</para>
</sect2>
<sect2 id="sect_09_05_02"><title>The continue built-in</title>
<para>The <command>continue</command> statement resumes iteration of an enclosing <command>for</command>, <command>while</command>, <command>until</command> or <command>select</command> loop.</para>
<para>When used in a <command>for</command> loop, the controlling variable takes on the value of the next element in the list. When used in a <command>while</command> or <command>until</command> construct, on the other hand, execution resumes with <command>TEST-COMMAND</command> at the top of the loop.</para>
</sect2>
<sect2 id="sect_09_05_03"><title>Examples</title>
<para>In the following example, file names are converted to lower case. If no conversion needs to be done, a <command>continue</command> statement restarts execution of the loop. These commands don't eat much system resources, and most likely, similar problems can be solved using <command>sed</command> and <command>awk</command>. However, it is useful to know about this kind of construction when executing heavy jobs, that might not even be necessary when tests are inserted at the correct locations in a script, sparing system resources.</para>
<screen>
<prompt>[carol@octarine ~/test]</prompt> <command>cat <filename>tolower.sh</filename></command>
#!/bin/bash
# This script converts all file names containing upper case characters into file# names containing only lower cases.
LIST="$(ls)"
for name in "$LIST"; do
if [[ "$name" != *[[:upper:]]* ]]; then
continue
fi
ORIG="$name"
NEW=`echo $name | tr 'A-Z' 'a-z'`
mv "$ORIG" "$NEW"
echo "new name for $ORIG is $NEW"
done
</screen>
<para>This script has at least one disadvantage: it overwrites existing files. The <option>noclobber</option> option to Bash is only useful when redirection occurs. The <option>-b</option> option to the <command>mv</command> command provides more security, but is only safe in case of one accidental overwrite, as is demonstrated in this test:</para>
<screen>
<prompt>[carol@octarine ~/test]</prompt> <command>rm <filename>*</filename></command>
<prompt>[carol@octarine ~/test]</prompt> <command>touch <filename>test Test TEST</filename></command>
<prompt>[carol@octarine ~/test]</prompt> <command>bash <option>-x</option> <filename>tolower.sh</filename></command>
++ ls
+ LIST=test
Test
TEST
+ [[ test != *[[:upper:]]* ]]
+ continue
+ [[ Test != *[[:upper:]]* ]]
+ ORIG=Test
++ echo Test
++ tr A-Z a-z
+ NEW=test
+ mv -b Test test
+ echo 'new name for Test is test'
new name for Test is test
+ [[ TEST != *[[:upper:]]* ]]
+ ORIG=TEST
++ echo TEST
++ tr A-Z a-z
+ NEW=test
+ mv -b TEST test
+ echo 'new name for TEST is test'
new name for TEST is test
<prompt>[carol@octarine ~/test]</prompt> <command>ls <option>-a</option></command>
./ ../ test test~
</screen>
<para>The <command>tr</command> is part of the <emphasis>textutils</emphasis> package; it can perform all kinds of character transformations.</para>
</sect2>
</sect1>
<sect1 id="sect_09_06"><title>Making menus with the select built-in</title>
<sect2 id="sect_09_06_01"><title>General</title>
<sect3 id="sect_09_06_01_01"><title>Use of select</title>
<para>The <command>select</command> construct allows easy menu generation. The syntax is quite similar to that of the <command>for</command> loop:</para>
<cmdsynopsis><command>select <varname>WORD</varname> [in <varname>LIST</varname>]; do RESPECTIVE-COMMANDS; done</command></cmdsynopsis>
<para><varname>LIST</varname> is expanded, generating a list of items. The expansion is printed to standard error; each item is preceded by a number. If <command>in <varname>LIST</varname></command> is not present, the positional parameters are printed, as if <command>in <varname>$@</varname></command> would have been specified. <varname>LIST</varname> is only printed once.</para>
<para>Upon printing all the items, the <varname>PS3</varname> prompt is printed and one line from standard input is read. If this line consists of a number corresponding to one of the items, the value of <varname>WORD</varname> is set to the name of that item. If the line is empty, the items and the <varname>PS3</varname> prompt are displayed again. If an <emphasis>EOF</emphasis> (End Of File) character is read, the loop exits. Since most users don't have a clue which key combination is used for the EOF sequence, it is more user-friendly to have a <command>break</command> command as one of the items. Any other value of the read line will set <varname>WORD</varname> to be a null string.</para>
<para>The read line is saved in the <varname>REPLY</varname> variable.</para>
<para>The <command>RESPECTIVE-COMMANDS</command> are executed after each selection until the number representing the <command>break</command> is read. This exits the loop.</para>
</sect3>
<sect3 id="sect_09_06_01_02"><title>Examples</title>
<para>This is a very simple example, but as you can see, it is not very user-friendly:</para>
<screen>
<prompt>[carol@octarine testdir]</prompt> <command>cat <filename>private.sh</filename></command>
#!/bin/bash
echo "This script can make any of the files in this directory private."
echo "Enter the number of the file you want to protect:"
select FILENAME in *;
do
echo "You picked $FILENAME ($REPLY), it is now only accessible to you."
chmod go-rwx "$FILENAME"
done
<prompt>[carol@octarine testdir]</prompt> <command>./private.sh</command>
This script can make any of the files in this directory private.
Enter the number of the file you want to protect:
1) archive-20030129
2) bash
3) private.sh
#? 1
You picked archive-20030129 (1)
#?
</screen>
<para>Setting the <varname>PS3</varname> prompt and adding a possibility to quit makes it better:</para>
<screen>
#!/bin/bash
echo "This script can make any of the files in this directory private."
echo "Enter the number of the file you want to protect:"
PS3="Your choice: "
QUIT="QUIT THIS PROGRAM - I feel safe now."
touch "$QUIT"
select FILENAME in *;
do
case $FILENAME in
"$QUIT")
echo "Exiting."
break
;;
*)
echo "You picked $FILENAME ($REPLY)"
chmod go-rwx "$FILENAME"
;;
esac
done
rm "$QUIT"
</screen>
</sect3>
</sect2>
<sect2 id="sect_09_06_02"><title>Submenus</title>
<para>Any statement within a <command>select</command> construct can be another <command>select</command> loop, enabling (a) submenu(s) within a menu.</para>
<para>By default, the <varname>PS3</varname> variable is not changed when entering a nested <command>select</command> loop. If you want a different prompt in the submenu, be sure to set it at the appropriate time(s).</para>
</sect2>
</sect1>
<sect1 id="sect_09_07"><title>The shift built-in</title>
<sect2 id="sect_09_07_01"><title>What does it do?</title>
<para>The <command>shift</command> command is one of the Bourne shell built-ins that comes with Bash. This command takes one argument, a number. The positional parameters are shifted to the left by this number, <emphasis>N</emphasis>. The positional parameters from <varname>N+1</varname> to <varname>$#</varname> are renamed to variable names from <varname>$1</varname> to <varname>$# - N+1</varname>.</para>
<para>Say you have a command that takes 10 arguments, and N is 4, then <varname>$4</varname> becomes <varname>$1</varname>, <varname>$5</varname> becomes <varname>$2</varname> and so on. <varname>$10</varname> becomes <varname>$7</varname> and the original <varname>$1</varname>, <varname>$2</varname> and <varname>$3</varname> are thrown away.</para>
<para>If N is zero or greater than <varname>$#</varname>, the positional parameters are not changed (the total number of arguments, see <xref linkend="sect_07_02_01_02" />) and the command has no effect. If N is not present, it is assumed to be 1. The return status is zero unless N is greater than <varname>$#</varname> or less than zero; otherwise it is non-zero.</para>
</sect2>
<sect2 id="sect_09_07_02"><title>Examples</title>
<para>A shift statement is typically used when the number of arguments to a command is not known in advance, for instance when users can give as many arguments as they like. In such cases, the arguments are usually processed in a <command>while</command> loop with a test condition of <command>(( $# ))</command>. This condition is true as long as the number of arguments is greater than zero. The <varname>$1</varname> variable and the <command>shift</command> statement process each argument. The number of arguments is reduced each time <command>shift</command> is executed and eventually becomes zero, upon which the <command>while</command> loop exits.</para>
<para>The example below, <filename>cleanup.sh</filename>, uses <command>shift</command> statements to process each file in the list generated by <command>find</command>:</para>
<screen>
#!/bin/bash
# This script can clean up files that were last accessed over 365 days ago.
USAGE="Usage: $0 dir1 dir2 dir3 ... dirN"
if [ "$#" == "0" ]; then
echo "$USAGE"
exit 1
fi
while (( "$#" )); do
if [[ $(ls "$1") == "" ]]; then
echo "Empty directory, nothing to be done."
else
find "$1" -type f -a -atime +365 -exec rm -i {} \;
fi
shift
done
</screen>
<note><title>-exec vs. xargs</title>
<para>The above <command>find</command> command can be replaced with the following:</para>
<cmdsynopsis><command>find <option>options</option> | xargs [commands_to_execute_on_found_files]</command></cmdsynopsis>
<para>The <command>xargs</command> command builds and executes command lines from standard input. This has the advantage that the command line is filled until the system limit is reached. Only then will the command to execute be called, in the above example this would be <command>rm</command>. If there are more arguments, a new command line will be used, until that one is full or until there are no more arguments. The same thing using <command>find <option>-exec</option></command> calls on the command to execute on the found files every time a file is found. Thus, using <command>xargs</command> greatly speeds up your scripts and the performance of your machine.</para>
</note>
<para>In the next example, we modified the script from <xref linkend="sect_08_02_04_04" /> so that it accepts multiple packages to install at once:</para>
<screen>
#!/bin/bash
if [ $# -lt 1 ]; then
echo "Usage: $0 package(s)"
exit 1
fi
while (($#)); do
yum install "$1" &lt;&lt; CONFIRM
y
CONFIRM
shift
done
</screen>
</sect2>
</sect1>
<sect1 id="sect_09_08"><title>Summary</title>
<para>In this chapter, we discussed how repetitive commands can be incorporated in loop constructs. Most common loops are built using the <command>for</command>, <command>while</command> or <command>until</command> statements, or a combination of these commands. The <command>for</command> loop executes a task a defined number of times. If you don't know how many times a command should execute, use either <command>until</command> or <command>while</command> to specify when the loop should end.</para>
<para>Loops can be interrupted or reiterated using the <command>break</command> and <command>continue</command> statements.</para>
<para>A file can be used as input for a loop using the input redirection operator, loops can also read output from commands that is fed into the loop using a pipe.</para>
<para>The <command>select</command> construct is used for printing menus in interactive scripts. Looping through the command line arguments to a script can be done using the <command>shift</command> statement.</para>
</sect1>
<sect1 id="sect_09_09"><title>Exercises</title>
<para>Remember: when building scripts, work in steps and test each step before incorporating it in your script.</para>
<orderedlist>
<listitem><para>Create a script that will take a (recursive) copy of files in <filename>/etc</filename> so that a beginning system administrator can edit files without fear.</para></listitem>
<listitem><para>Write a script that takes exactly one argument, a directory name. If the number of arguments is more or less than one, print a usage message. If the argument is not a directory, print another message. For the given directory, print the five biggest files and the five files that were most recently modified.</para></listitem>
<listitem><para>Can you explain why it is so important to put the variables in between double quotes in the example from <xref linkend="sect_09_04_02" />?</para></listitem>
<listitem><para>Write a script similar to the one in <xref linkend="sect_09_05_01" />, but think of a way of quitting after the user has executed 3 loops.</para></listitem>
<listitem><para>Think of a better solution than <command>move <option>-b</option></command> for the script from <xref linkend="sect_09_05_03" /> to prevent overwriting of existing files. For instance, test whether or not a file exists. Don't do unnecessary work!</para></listitem>
<listitem><para>Rewrite the <filename>whichdaemon.sh</filename> script from <xref linkend="sect_07_02_04" />, so that it:</para>
<itemizedlist>
<listitem><para>Prints a list of servers to check, such as Apache, the SSH server, the NTP daemon, a name daemon, a power management daemon, and so on.</para></listitem>
<listitem><para>For each choice the user can make, print some sensible information, like the name of the web server, NTP trace information, and so on.</para></listitem>
<listitem><para>Optionally, build in a possibility for users to check other servers than the ones listed. For such cases, check that at least the given process is running.</para></listitem>
<listitem><para>Review the script from <xref linkend="sect_09_02_02_04" />. Note how character input other than <keycap>q</keycap> is processed. Rebuild this script so that it prints a message if characters are given as input.</para></listitem>
</itemizedlist>
</listitem>
</orderedlist>
</sect1>
</chapter>