Softpanorama
May the source be with you, but remember the KISS principle ;-)

Contents Bulletin Scripting in shell and Perl Network troubleshooting History Humor

Loops in Bash

News

bash Control Structures

Recommended Links

For loop Examples of for loops While and until loops Examples of while loops
Arithmetic expressions Comparison operators Seq command Usage of pipes with loops
String Operations in Shell exec command BASH Debugging Shell history Tips Humor Etc

Note: Materials of this lecture notes combine copyrighted materials from Learning the Korn Shell (A Nutshell Handbook) by Bill Rosenblatt (first edition, 1993) and examples from Advanced Bash-Scripting Guide. This lecture notes were created by the author for his Unix shell course in FDU. As they include copyrighted materials they should be used strictly for educational purposes.

Introduction

A loop is a block of code that iterates (repeats) a list of commands as long as the loop control condition is true.

Borne shell introduced several types of loops, that while backward and brain-dead by programming language design standards were pretty innovative and powerful for shells of its time (although they were weaker AWK constructs which was created approximately at the same time). Now they looks completely archaic and syntax looks like a perversion, but we have what we have. In you can't overcome allergy please use Perl or other more modern scripting language. One very annoying wart is that until loop is implemented incorrectly in all shells since Borne shell (it is essentially while loop with negated condition, so the test is above the loop body, not below, like in "proper" until loop).

Most current version of shells (bash and ksh93) support three original types of loop from Born shell (with its unique Algol-68 style syntax; what is funny that while Stephen Bourne managed to push this syntax he was never able to provide a distinct lexical level for the shell. So, if analogies with Algol 68 control structures exists, they are all superficial.

The control structures in shell by-and-large were preserved in the form they were introduced. So the progress here stopped in late 70th of the last century. Some technical decisions look very strange or arbitrary or both even for this time and even taking compatibility pressure into account. My impression is that Borne shell is never designed, it was just a rather primitive hack of existing Thomson/Mashey shell shell. As any hack it was done on far lower level of architectural thinking in comparison with the design of awk, which, paradoxically, was also the product of Bell labs. It's kind of strange that two teams working in the same lab on similar project never cooperated; that happens often. But the fact that managers did not realize the potential of awk and continued with Borne shell instead of throwing it into a garbage can and extending awk to the shell level tells something about quality of management. Truth be told, management in IT project is always a problems and Unix team did not have the worst managers possible ;-). Actually it was due to Doug McIlroy, then a department head in the Computing Science Research Center, that Unix got pipes concept. Please try to find another manager who make similar level contribution to such a project.

In any case, you need to be patient and avoid crying "What a crap you are teaching us !!!" at the lectures ;-). Borne shell survived to those days in some minimally extended form and we have what we have. Humans are very adaptable and get used to things even if they look very unnatural and obscure at the beginning, especially if they are well paid for those skills :-).

At the same time modern shells, like bash 3.x and later support some extensions like proper C-style for loop.

Here is the list of loops that modern shells such as bash 3.x and ksh93 support.

while loop

Bash provides while loops (loop which has the test condition at the top), but not until loop (loop with test condition at the bottom, after the loop body). To make things confusing there is a construct called until loop but it is implemented incorrectly and has the same test at the top as while loop, unlike loop constructs in Pascal (while/do and repeat/until) and C (while and do/until). This is a legacy of Borne shell (which has a really horrible design) and it is amazing how such incorrect solution survived for so long (the notion of feature deprecation was unknown in 70th and 80th; but it is strange that it was unknown by POSIX committee (committee to defeat alliance of Sun and AT&T ;-). The sad fact that it is still not widely used in shell designer world even now, 40 years since shell was created.

In shell the until condition is checked at the top of the loop, not at the bottom as it is in analogous constructs in C and Pascal. So this is just a while loop with reversed check for termination

As a result of this idiosyncrasy you can convert any until loop into a while loop by simply negating the condition. Therefore we will ignore the existence of until loop throughout the rest of course.

The syntax for while is:

while condition
do
    statements...
done

Here condition is either a command(s) and/or shell conditional operator. There is no limit on the number commands in the condition (return code from the last one is used to check for termination) or complexity of the conditional operators. They also can be combined (see below). At the same time while and until loops are actually most useful when conditions are simple. For example

while ping -c 1 $ip 
do
   sleep 60;
done
echo "Connectivity to $ip lost"

You can reverse the condition using until instead of while (remember that until in shell is just while with reversed condition of success):

until ping -c 1 $ip  # 
do
   echo $ip still down
   sleep 60;
done
echo "Server $ip rebooted"

Please note that in the past they often were used to imitate C-style for loop, for example:

i=1
while (( $i < 10 )); do
	echo i=$i
	let i++
done

But now it is unnecessary now as bash since version 2.04 (that means "for a long time" as current version is 4.x) has a "Classic C-style" for loop construct . See C-style for loop below. While loop is often used to read the file like by line and perform some action on lines:

exec <$file # redirects stdin to a file
while read line # read one line
do
   echo $line
done

The trick here is the usage of exec command to redirect file to STDIN. See exec commnad for more details but in essence it is equivalent to invocation of the script like:

./somescript < inputfile

The script can accomplish the same thing internally by using: exec < inputfile. After that line, the script's input is inputfile. It can do the same to output:

exec > somescript.log 2>&1

This send stderr and stdout combined into the file. There is more to it, but this is the basics.

As a more complex example let's discuss an implementation of a simplified version of the shell's built-in whence command. By "simplified," we mean that we will implement only the part that checks all of the directories in your PATH for the command you give as argument (we won't implement checking for aliases, built-in commands, etc.).

The simplest way to do it is to picking the directories in PATH one by one and check if the file is present in it. To extract the directory we will use the shell's pattern-matching operator %% , and seeing if there is a file with the given name in the directory that you have permission to execute. Here is the code:

mypath=$PATH:
while [[ -n $mypath ]]; do
    mydir=${mypath%%:*}
    if [[ -x $mydir/$1 && ! -d $mydir/$1 ]]; then
        print "$mydir/$1"
        return
    fi
    mypath=${mypath#*:}  
done
return 1

The only things that might need commentary here are regular expressions:

mypath=${mypath#*:}
mydir=${mypath%%:*}

The first of these uses another shell string operator: this one deletes the shortest match to the pattern given from the front of the string. By now, this type of operator should be familiar. This line deletes the front directory from $path and assigns the result back to path. The second line is the same as before the while: it finds the (new) front directory in $path and assigns it to dir. This sets up the loop for another iteration.

Thus, the code loops through all of the directories in PATH. It exits when it finds a matching executable file or when it has "eaten up" the entire PATH. If no matching executable file is found, it prints nothing and exits with an error status.

We can enhance this script a bit by taking advantage of the UNIX utility file(1). Command file examines files given as arguments and determines what type they are, based on the file's magic number and various heuristics (educated guesses). A magic number is a field in the header of an executable file that the linker sets to identify what type of executable it is.

If filename is an executable program (compiled from C or some other language), then typing file filename produces output similar to this:

filename: ELF 32-bit LSB executable 80386 Version 1

However, if filename is not an executable program, it will examine the first few lines and try to guess what kind of information the file contains. If the file contains text (as opposed to binary data), file will look for indications that it is English, shell commands, C, FORTRAN, troff(1) input, and various other things. file guess wrong sometimes, but it is mostly correct.

We can just substitute file for print to print a more informative message in our script:

path=$PATH
dir=${path%%:*}
while [[ -n $path ]]; do
    if [[ -x $dir/$1 && ! -d $dir/$1 ]]; then
	  file $dir/$1
	  return
    fi
    path=${path#*:}
    dir=${path%%:*}
done
return 1

Assume that fred is an executable file in the directory /usr/bin, and that bob is a shell script in /usr/local/bin. Then typing file fred produces this output:

/usr/bin/fred: ELF 32-bit LSB executable 80386 Version 1

And typing file bob has this result:

/usr/local/bin/bob: commands text

Notice that the statement dir=${path%%:*} appears in two places, before the start of the loop and as the last statement in the loop's body. As you can use multiple statements after the keyword while it allow programmers to create C-style while loops:

while iterative-step; condition; do
    ...
done

For example

path=$PATH
while dir=${path%%:*};  [[ -n $path ]]; do
    if [[ -x $dir/$1 && ! -d $dir/$1 ]]; then
	file $dir/$1
	return
    fi
    path=${path#*:}
done
return 1

Note that there are two statements in while header but only return code from the last statement control the loop.

Breaking out of a Loop

To break out of a loop, the command word break is used. Command execution will continue with the first command line found after the end of the loop (after the word "done").

If loops are nested, it is possible to break out not only out of the current but out of any number of nested loops by following the word break with the number of loops to break out of.

while true
do

command 1

for variable in one two three four five
do
echo "$variable"
if [[ "$variable" = "four" ]]
then
break
fi
done

command n # <<<<<break statement passes control to this line <<<<

done
another_command_line

while true
do

command 1

for variable in one two three four five
do
echo "$variable"
if [[ "$variable" = "four" ]]
then
break 2
fi
done

command n

done
another_command_line # <<<<<break 2 statement passes control to this line <<<<

Continuing to the next iteration

It is possible to force the to skip the rest of loop body and continue to the next iteration. While this statement traditionally called continue (as in C) it is actually more accurately can be called the next statement (as in Perl).

for level1_var in one two three four five
do
echo "$level1_var"
for level2_var in a b c d
do # <<<<continue statement passes control to this line

echo "$level1_var $level2_var"
if [[ "$level2_var" = "c" ]] ; then
continue
fi
commands_in_nested_loop

done
done

The continue statement can have argument which can be used for escaping from nested loop. It specifies the number of levels to continue the next operation.

for level1_var in one two three four five
do # <<<<continue 2 statement passes control to this line
echo "$level1_var"
for level2_var in a b c d
do # <<<<continue statement passes control to this line

echo "$level1_var $level2_var"
if [[ "$level2_var" = "c" ]] ; then
continue 2
fi
commands_in_nested_loop

done
done

Examples of while Loops
from Advanced BASH Programming Guide

var0=0
LIMIT=10

while [ "$var0" -lt "$LIMIT" ]
do
  echo -n "$var0 "        # -n suppresses newline.
  #             ^ Space is used to separate printed out numbers.

  var0=`expr $var0 + 1`   # var0=$(($var0+1))  also works.
                          # var0=$((var0 + 1)) also works.
                          # let "var0 += 1"    also works.
done                      # Various other methods also work.

echo

exit 0

Another while loop

while [ "$var1" != "end" ]     # while test "$var1" != "end"
do                             # also works.
  echo "Input variable #1 (end to exit) "
  read var1                    # Not 'read $var1' (shell idiosyncrasy).
  echo "variable #1 = $var1"   # Need quotes because of "#".
  # If input is 'end', echoes it here.
  # Does not test for termination condition until top of loop.
  echo
done  

exit 0

A while loop may have multiple conditions. Only the final condition determines when the loop terminates. This necessitates a slightly different loop syntax, however.

while >loop with multiple conditions

var1=unset
previous=$var1

while echo "previous-variable = $previous"
      echo
      previous=$var1
      [ "$var1" != end ] # Keeps track of what $var1 was previously.
      # Four conditions on "while", but only last one controls loop.
      # The *last* exit status is the one that counts.
do
echo "Input variable #1 (end to exit) "
  read var1
  echo "variable #1 = $var1"
done  

# Try to figure out how this all works.
# It's a wee bit tricky.

exit 0

A while loop may employ C-like syntax by using the double parentheses construct :

# wh-loopc.sh: Count to 10 in a "while" loop.

LIMIT=10
a=1

while [ "$a" -le $LIMIT ]
do
  echo -n "$a "
  let "a+=1"
done           # No surprises, so far.

echo; echo

# +=================================================================+

# Now, repeat with C-like syntax.

((a = 1))      # a=1
# Double parentheses permit space when setting a variable, as in C.

while (( a <= LIMIT ))   # Double parentheses, and no "$" preceding variables.
do
  echo -n "$a "
  ((a += 1))   # let "a+=1"
  # Yes, indeed.
  # Double parentheses permit incrementing a variable with C-like syntax.
done

echo

# Now, C programmers can feel right at home in Bash.

exit 0

Examples of until Loops from Advanced BASH Programming Guide

#!/bin/bash

END_CONDITION=end

until [ "$var1" = "$END_CONDITION" ]
# Tests condition here, at top of loop.
do
  echo "Input variable #1 "
  echo "($END_CONDITION to exit)"
  read var1
  echo "variable #1 = $var1"
  echo
done  

exit 0

For loops

In bash there are two types of for loops: "old" and "new" (C-style). The latter is much better and unless you need to do something for which old for loop is especially suitable you should avoid using old syntax. Forget about silly noise about compatibility. Bash is now standard de-facto and is available of most operating systems. It is still not available by default on HP-UX 11, but precompiled version exists and is easily installable. It is easier to install bash on all systems that struggle with compatibility questions (ksh93 is an alternative and its more reliable).

"Old" for loop

This type of for look is a bastard child of the initial versions of shell when the key requirement was small size of executable. Like many bad design decisions it survived in old subsequent version. The for loop allows you to repeat a section of code a fixed number of times. During each time through the code (known as an iteration), a special variable called a loop variable is set to a different value; this way each iteration can do something slightly different.

The for loop used in Born shell is different from for look used in C. The main difference is that the shell's for loop doesn't let you specify a number interactions and counter. It iterated thought the list which can be supplied in several different ways:

The syntax for the for loop in shell is close to foreach loop in Perl:

for name [in list ]
do
    statements that can use  varibale $name, which changes on each iteration...
done

The list is a list of names. (If in list is omitted, the list defaults to "$@", i.e., the quoted list of command-line arguments, but you should always supply the in list for the sake of clarity.). For example

for user in nick serg dave leo bob john
do
    grep $user /etc/passwd
done

The list with elements over which we iterate can be represented by a variable:

USER_NAMES="nick serg dave leo bob john" 
for sys in $USER_NAMES ; do
    grep $user /etc/passwd 
done

The [list] in a for loop may contain filename globbing, that is, using wildcards for filename expansion.

The * in the for construct is not the same as $*. It's a wildcard, i.e., all files in a directory.

CP=$(which cp)
for f in * ; do
        if [ -f ${f}.bak ]
        then
                echo "skiping $f file"
                continue # read the next file
        fi
        $CP $f $f.bak
done;

Another similar example:

for myfile in /etc/r* ; do
    if [ -d "$myfile" ] 
    then
      echo "directory $myfile"
    else
      echo "\t$myfile"
    fi
done

As an example let's create a script that prints the type of each file passed as parameter:

for filename in "$@" ; do
    finfo $filename
    print
done

The loop calls subroutine finfo, which we now need to define:

function finfo {
    if [[ ! -a $1 ]]; then
        print "file $1 does not exist."
        return 1
    fi
    file $1
}

The complete script consists of the for loop code and the above function, in either order; good programming style dictates that the function definition should go first.

Using seq command with for loop you can generate simple sequences programmatically:

#!/bin/bash
for i in `seq 1 10`; do
   echo $i
done

This example is trivial to replicate with while loop with counter command. But you can also generate sequences of floating-point numbers, for example :

for flc in $(seq 1.0 .01 1.1)
do
   echo $flc; 
done

Here is more complex example borrowed from Learning the Korn Shell (A Nutshell Handbook)

Your UNIX system has the ability to transfer files from an MS-DOS system, but it leaves the DOS filenames intact. Write a script that translates the filenames in a given directory from DOS format to a more UNIX-friendly format.

DOS filenames have the format FILENAME.EXT. FILENAME can be up to eight characters long; EXT is an extension that can be up to three characters. The dot is required even if the extension is null; letters are all uppercase. We want to do the following:

  1. Translate letters from uppercase to lowercase.

  2. If the extension is null, remove the dot.

The first tool we will need for this job is the UNIX tr(1) utility, which translates characters on a one-to-one basis. Given the arguments charset1 and charset2, it will translate characters in the standard input that are members of charset1 into corresponding characters in charset2. The two sets are ranges of characters enclosed in square brackets ([] in standard regular-expression form in the manner of grep, awk, ed, etc.). More to the point, tr [A-Z] [a-z] takes its standard input, converts uppercase letters to lowercase, and writes the converted text to the standard output.

That takes care of the first step in the translation process. We can use a Korn shell string operator to handle the second. Here is the code for a script we'll call dosmv:

for filename in ${1:+$1/}* ; do
    newfilename=$(print $filename | tr [A-Z] [a-z])
    newfilename=${newfilename%.}
    print "$filename -> $newfilename"
    mv $filename $newfilename
done

This script accepts a directory name as argument, the default being the current directory. The expression ${1:+$1/} evaluates to the argument ($1) with a slash appended if the argument is supplied, or the null string if it isn't supplied. So the entire expression ${1:+$1/}* evaluates to all files in the given directory, or all files in the current directory if no argument is given.

Therefore, filename takes on the value of each filename in the list. filename gets translated into newfilename in two steps. (We could have done it in one, but readability would have suffered.) The first step uses tr in a pipeline within a command substitution construct. Our old friend print makes the value of filename the standard input to tr. tr's output becomes the value of the command substitution expression, which is assigned to newfilename. Thus, if $filename were DOSFILE.TXT, newfilename would become dosfile.txt.

The second step uses one of the shell's pattern-matching operators, the one that deletes the shortest match it finds at the end of the string. The pattern here is ., which means a dot at the end of the string. This means that the expression ${newfilename%.} will delete a dot from $newfilename only if it's at the end of the string; otherwise the expression will leave $newfilename intact. For example, if $newfilename is dosfile.txt, it will be untouched, but if it's dosfile., the expression will change it to dosfile without the final dot. In either case, the new value is assigned back to newfilename.

UNIX regular expression mavens should remember that this is shell wildcard syntax, in which dots are not operators and therefore do not need to be backslash-escaped.

The last statement in the for loop body does the file renaming with the standard UNIX mv(1) command. Before that, a print command simply informs the user of what's happening.

There is one little problem with the solution on the previous page: if there are any files in the given directory that aren't DOS files (in particular, if there are files whose names don't contain uppercase letters and don't contain a dot), then the conversion will do nothing to those filenames and mv will be called with two identical arguments. mv will complain with the message: mv: filename and filename are identical. We can solve this problem by letting grep determine whether each file has a DOS filename or not. The grep regular expression:

[^a-z]\{1,8\}\.[^a-z]\{0,3\}

is adequate (for these purposes) for matching DOS-format filenames. The character class [^a-z] means "any character except a lowercase letter." So the entire regular expression means: "Between 1 and 8 non-lowercase letters, followed by a dot, followed by 0 to 3 non-lowercase letters."

When grep runs, it normally prints all of the lines in its standard input that match the pattern you give it as argument. But we only need it to test whether or not the pattern is matched. Luckily, grep's exit status is "well-behaved": it's 0 if there is a match in the input, 1 if not. Therefore, we can use the exit status to test for a match. We also need to discard grep's output; to do this, we redirect it to the special file /dev/null, which is colloquially known as the "bit bucket." Any output directed to /dev/null effectively disappears. Thus, the command line:

print "$filename" | grep '[^a-z]\{1,8\}\.[^a-z]\{0,3\}' > /dev/null

Some Berkeley-derived versions of UNIX have a -s ("silent") option to grep that suppresses standard output, thereby making redirection to /dev/null unnecessary.

prints nothing and returns exit status 0 if the filename is in DOS format, 1 if not.

As an exercise please modify our ren script to incorporate so that the argument list may contain wild cards.

New (C-style) for loop

In bash (since version 2.04) new form of for syntax, one that looks a lot like C Language, but with double parentheses, was introduced:

for (( i=0 ; i < 10 ; i++ )) ; do echo $i ; done

Its more general form can be described as:

for (( expr1 ; expr2 ; expr3 )) ; do list ; done

The use of double parentheses indicates that expressions can use syntax of ((..)) construct.

Here is example how to shutdown Windows XP after certain time (in the example below 10 min) interval using shutdown command:

for (( i = 1; i <= 10; i++ )) ; do
    sleep 60
done
shutdown -s

Several iteration counters can be used. For example:

for (( i=0, j=10 ; i < 10 ; i++, j-- ))
do
    echo $i $j
done

That for loop initializes two variables (i and j), then has a more complex second expression adding the two together before doing the less-than comparison. The comma operator is used again in the third expression to increment both variables.

Like "while true" C-style for loop can be used for creation of "infinite loops", loops which end (break) due to Ctrl-C entered or due to execution of some internal statement that passes control outside the loop, not because of the condition specified in loop header. For example

for (( ; ; ))
do
   sleep 60
   echo `date` "[Hit CTRL+C to stop]"
done

Examples of for loops
from Advanced BASH programming Guide

#!/bin/bash
# Listing the planets.

for planet in Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune Pluto
do
  echo $planet  # Each planet on a separate line.
done

echo

for planet in "Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune Pluto"
# All planets constitute a single argument and will be printed on same line.
do
  echo $planet
done

exit 0

Each element in double or single quotes can contain multiple words. Those words can besplit in the inner loop. See Example 11-14) to force parsing of each [list] element and assignment of each component to the positional parameters.

for loop with two parameters in each [list] element

#!/bin/bash
# Planets revisited.

# Associate the name of each planet with its distance from the sun.

for planet in "Mercury 36" "Venus 67" "Earth 93"  "Mars 142" "Jupiter 483"
do
  set -- $planet  # Parses variable "planet" and sets positional parameters.
  # the "--" prevents nasty surprises if $planet is null or begins with a dash.

  # May need to save original positional parameters, since they get overwritten.
  # One way of doing this is to use an array,
  #        original_params=("$@")

  echo "$1		$2,000,000 miles from the sun"
  #-------two  tabs---concatenate zeroes onto parameter $2
done

# (Thanks, S.C., for additional clarification.)

exit 0

A variable may supply the [list] in a for loop.

Fileinfo: operating on a file list contained in a variable

#!/bin/bash
# fileinfo.sh

FILES="/usr/sbin/privatepw
/usr/sbin/pwck
/usr/sbin/go500gw
/usr/bin/fakefile
/sbin/mkreiserfs
/sbin/ypbind"     # List of files you are curious about.
                  # Threw in a dummy file, /usr/bin/fakefile.

echo

for file in $FILES
do

  if [ ! -e "$file" ]       # Check if file exists.
  then
    echo "$file does not exist."; echo
    continue                # On to next.
   fi

  ls -l $file | awk '{ print $9 "         file size: " $5 }'  # Print 2 fields.
  whatis `basename $file`   # File info.
  echo
done  

exit 0

The [list] in a for loop may contain filename globbing, that is, using wildcards for filename expansion.

Operating on files with a for loop

#!/bin/bash
# list-glob.sh: Generating [list] in a for-loop using "globbing".

echo

for file in *
do
  ls -l "$file"  # Lists all files in $PWD (current directory).
  # Recall that the wild card character "*" matches every filename,
  # however, in "globbing", it doesn't match dot-files.

  # If the pattern matches no file, it is expanded to itself.
  # To prevent this, set the nullglob option
  # (shopt -s nullglob).
  # Thanks, S.C.
done

echo; echo

for file in [jx]*
do
  rm -f $file    # Removes only files beginning with "j" or "x" in $PWD.
  echo "Removed file \"$file\"".
done

echo

exit 0

Omitting the in [list] part of a for loop causes the loop to operate on $@, the list of arguments given on the command line to the script.

A particularly clever illustration of this is Example A-17.

Missing in [list] in a for loop

#!/bin/bash

#  Invoke this script both with and without arguments,
#+ and see what happens.

for a
do
 echo -n "$a "
done

#  The 'in list' missing, therefore the loop operates on '$@'
#+ (command-line argument list, including whitespace).

echo

exit 0

It is possible to use command substitution to generate the [list] in a for loop.

Generating the [list] in a for loop with command substitution

#!/bin/bash
#  for-loopcmd.sh: for-loop with [list]
#+ generated by command substitution.

NUMBERS="9 7 3 8 37.53"

for number in `echo $NUMBERS`  # for number in 9 7 3 8 37.53
do
  echo -n "$number "
done

echo 
exit 0

This is a somewhat more complex example of using command substitution to create the [list].

A grep replacement for binary files

#!/bin/bash
# bin-grep.sh: Locates matching strings in a binary file.

# A "grep" replacement for binary files.
# Similar effect to "grep -a"

E_BADARGS=65
E_NOFILE=66

if [ $# -ne 2 ]
then
  echo "Usage: `basename $0` search_string filename"
  exit $E_BADARGS
fi

if [ ! -f "$2" ]
then
  echo "File \"$2\" does not exist."
  exit $E_NOFILE
fi 

IFS="\n"         # Per suggestion of Paulo Marcel Coelho Aragao.
for word in $( strings "$2" | grep "$1" )
# The "strings" command lists strings in binary files.
# Output then piped to "grep", which tests for desired string.
do
  echo $word
done

# As S.C. points out, lines 23 - 29 could be replaced with the simpler
#    strings "$2" | grep "$1" | tr -s "$IFS" '[\n*]'


# Try something like  "./bin-grep.sh mem /bin/ls"  to exercise this script.

exit 0

More of the same.

Listing all users on the system

#!/bin/bash
# userlist.sh

PASSWORD_FILE=/etc/passwd
n=1           # User number

for name in $(awk 'BEGIN{FS=":"}{print $1}' < "$PASSWORD_FILE" )
# Field separator = :    ^^^^^^
# Print first field              ^^^^^^^^
# Get input from password file               ^^^^^^^^^^^^^^^^^
do
  echo "USER #$n = $name"
  let "n += 1"
done  


# USER #1 = root
# USER #2 = bin
# USER #3 = daemon
# ...
# USER #30 = bozo

exit 0

A final example of the [list] resulting from command substitution.

Checking all the binaries in a directory for authorship

#!/bin/bash
# findstring.sh:
# Find a particular string in binaries in a specified directory.

directory=/usr/bin/
fstring="Free Software Foundation"  # See which files come from the FSF.

for file in $( find $directory -type f -name '*' | sort )
do
  strings -f $file | grep "$fstring" | sed -e "s%$directory%%"
  #  In the "sed" expression,
  #+ it is necessary to substitute for the normal "/" delimiter
  #+ because "/" happens to be one of the characters filtered out.
  #  Failure to do so gives an error message (try it).
done  

exit 0

#  Exercise (easy):
#  ---------------
#  Convert this script to taking command-line parameters
#+ for $directory and $fstring.

The output of a for loop may be piped to a command or commands.

Listing the symbolic links in a directory

#!/bin/bash
# symlinks.sh: Lists symbolic links in a directory.


directory=${1-`pwd`}
#  Defaults to current working directory,
#+ if not otherwise specified.
#  Equivalent to code block below.
# ----------------------------------------------------------
# ARGS=1                 # Expect one command-line argument.
#
# if [ $# -ne "$ARGS" ]  # If not 1 arg...
# then
#   directory=`pwd`      # current working directory
# else
#   directory=$1
# fi
# ----------------------------------------------------------

echo "symbolic links in directory \"$directory\""

for file in "$( find $directory -type l )"   # -type l = symbolic links
do
  echo "$file"
done | sort                                  # Otherwise file list is unsorted.
#  Strictly speaking, a loop isn't really necessary here,
#+ since the output of the "find" command is expanded into a single word.
#  However, it's easy to understand and illustrative this way.

#  As Dominik 'Aeneas' Schnitzer points out,
#+ failing to quote  $( find $directory -type l )
#+ will choke on filenames with embedded whitespace.
#  Even this will only pick up the first field of each argument.

exit 0


# Jean Helou proposes the following alternative:

echo "symbolic links in directory \"$directory\""
# Backup of the current IFS. One can never be too cautious.
OLDIFS=$IFS
IFS=:

for file in $(find $directory -type l -printf "%p$IFS")
do     #                              ^^^^^^^^^^^^^^^^
       echo "$file"
       done|sort

The stdout of a loop may be redirected to a file, as this slight modification to the previous example shows.

Symbolic links in a directory, saved to a file

#!/bin/bash
# symlinks.sh: Lists symbolic links in a directory.

OUTFILE=symlinks.list                         # save file

directory=${1-`pwd`}
#  Defaults to current working directory,
#+ if not otherwise specified.


echo "symbolic links in directory \"$directory\"" > "$OUTFILE"
echo "---------------------------" >> "$OUTFILE"

for file in "$( find $directory -type l )"    # -type l = symbolic links
do
  echo "$file"
done | sort >> "$OUTFILE"                     # stdout of loop
#           ^^^^^^^^^^^^^                       redirected to save file.

exit 0

There is an alternative syntax to a for loop that will look very familiar to C programmers. This requires double parentheses.

A C-like for loop

#!/bin/bash
# Two ways to count up to 10.

echo

# Standard syntax.
for a in 1 2 3 4 5 6 7 8 9 10
do
  echo -n "$a "
done  

echo; echo

# +==========================================+

# Now, let's do the same, using C-like syntax.

LIMIT=10

for ((a=1; a <= LIMIT ; a++))  # Double parentheses, and "LIMIT" with no "$".
do
  echo -n "$a "
done                           # A construct borrowed from 'ksh93'.

echo; echo

# +=========================================================================+

# Let's use the C "comma operator" to increment two variables simultaneously.

for ((a=1, b=1; a <= LIMIT ; a++, b++))  # The comma chains together operations.
do
  echo -n "$a-$b "
done

echo; echo

exit 0

---

Now, a for-loop used in a "real-life" context.


Top updates

Bulletin Latest Past week Past month
Google Search


NEWS CONTENTS

Old News ;-)

[Sep 10, 2010] bash iterator trick

The UNIX Blog

A neat little feature I never new existed in bash is being able to iterate over a sequence of number in a more or less C-esque manner. Coming from Bourne/Korn shell background creating an elegant iterator is always a slight nuisance, since you would come up with something like this to iterate over a sequence of numbers:

i=1;
while [ $i -lt 10 ]; do
i=`expr $i + 1`;
.....
done

Well, not exactly the most elegant solution. With bash on the other hand it can be done as simple as:

for((i=1; $i<10; i++)); do
....
done

Simple and to the point.

Bash by example, Part 2

The standard "for" loop in bash is pretty idiosyncratic
OK, we've covered conditionals, now it's time to explore bash looping constructs. We'll start with the standard "for" loop. Here's a basic example:
#!/usr/bin/env bash

for x in one two three four
do
    echo number $x
done

output:
number one
number two 
number three 
number four

What exactly happened? The "for x" part of our "for" loop defined a new environment variable (also called a loop control variable) called "$x", which was successively set to the values "one", "two", "three", and "four". After each assignment, the body of the loop (the code between the "do" ... "done") was executed once. In the body, we referred to the loop control variable "$x" using standard variable expansion syntax, like any other environment variable. Also notice that "for" loops always accept some kind of word list after the "in" statement. In this case we specified four English words, but the word list can also refer to file(s) on disk or even file wildcards. Look at the following example, which demonstrates how to use standard shell wildcards:

#!/usr/bin/env bash

for myfile in /etc/r*
do
    if [ -d "$myfile" ] 
    then
      echo "$myfile (dir)"
    else
      echo "$myfile"
    fi
done

output:

/etc/rc.d (dir)
/etc/resolv.conf
/etc/resolv.conf~
/etc/rpc                  

The above code looped over each file in /etc that began with an "r". To do this, bash first took our wildcard /etc/r* and expanded it, replacing it with the string /etc/rc.d /etc/resolv.conf /etc/resolv.conf~ /etc/rpc before executing the loop. Once inside the loop, the "-d" conditional operator was used to perform two different actions, depending on whether myfile was a directory or not. If it was, a " (dir)" was appended to the output line.

We can also use multiple wildcards and even environment variables in the word list:

for x in /etc/r??? /var/lo* /home/drobbins/mystuff/* /tmp/${MYPATH}/*
do
    cp $x /mnt/mydir
done

Bash will perform wildcard and variable expansion in all the right places, and potentially create a very long word list.

While all of our wildcard expansion examples have used absolute paths, you can also use relative paths, as follows:

for x in ../* mystuff/*
do
    echo $x is a silly file
done

In the above example, bash performs wildcard expansion relative to the current working directory, just like when you use relative paths on the command line. Play around with wildcard expansion a bit. You'll notice that if you use absolute paths in your wildcard, bash will expand the wildcard to a list of absolute paths. Otherwise, bash will use relative paths in the subsequent word list. If you simply refer to files in the current working directory (for example, if you type "for x in *"), the resultant list of files will not be prefixed with any path information. Remember that preceding path information can be stripped using the "basename" executable, as follows:

for x in /var/log/*
do
    echo `basename $x` is a file living in /var/log
done

Of course, it's often handy to perform loops that operate on a script's command-line arguments. Here's an example of how to use the "$@" variable, introduced at the beginning of this article:

#!/usr/bin/env bash

for thing in "$@"
do
    echo you typed ${thing}.
done

output:

$ allargs hello there you silly
you typed hello.
you typed there.
you typed you.
you typed silly.

Recommended Links

Softpanorama Top Visited

Softpanorama Recommended




Etc

Society

Groupthink : Understanding Micromanagers and Control Freaks : Toxic Managers : BureaucraciesHarvard Mafia : Diplomatic Communication : Surviving a Bad Performance Review : Insufficient Retirement Funds as Immanent Problem of Neoliberal Regime : PseudoScience : Who Rules America : Two Party System as Polyarchy : Neoliberalism  : The Iron Law of Oligarchy : Libertarian Philosophy

Quotes

Skeptical Finance : John Kenneth Galbraith : Keynes : George Carlin : Skeptics : Propaganda  : SE quotes : Language Design and Programming Quotes : Random IT-related quotes : Oscar Wilde : Talleyrand : Somerset Maugham : War and Peace : Marcus Aurelius : Eric Hoffer : Kurt Vonnegut : Otto Von Bismarck : Winston Churchill : Napoleon Bonaparte : Ambrose Bierce : Oscar Wilde : Bernard Shaw : Mark Twain Quotes

Bulletin:

Vol 26, No.1 (January, 2013) Object-Oriented Cult : Vol 25, No.12 (December, 2013) Rational Fools vs. Efficient Crooks: The efficient markets hypothesis : Vol 25, No.08 (August, 2013) Cloud providers as intelligence collection hubs : Vol 23, No.10 (October, 2011) An observation about corporate security departments : Vol 23, No.11 (November, 2011) Softpanorama classification of sysadmin horror stories : Vol 25, No.05 (May, 2013) Corporate bullshit as a communication method : Vol 25, No.10 (October, 2013) Cryptolocker Trojan (Win32/Crilock.A) : Vol 25, No.06 (June, 2013) A Note on the Relationship of Brooks Law and Conway Law

History:

Fifty glorious years (1950-2000): the triumph of the US computer engineering : Donald Knuth : TAoCP and its Influence of Computer Science : Richard Stallman : Linus Torvalds  : Larry Wall  : John K. Ousterhout : CTSS : Multix OS Unix History : Unix shell history : VI editor : History of pipes concept : Solaris : MS DOSProgramming Languages History : PL/1 : Simula 67 : C : History of GCC developmentScripting Languages : Perl history   : OS History : Mail : DNS : SSH : CPU Instruction Sets : SPARC systems 1987-2006 : Norton Commander : Norton Utilities : Norton Ghost : Frontpage history : Malware Defense History : GNU Screen : OSS early history

Classic books:

The Peter Principle : Parkinson Law : 1984 : The Mythical Man-MonthHow to Solve It by George Polya : The Art of Computer Programming : The Elements of Programming Style : The Unix Haterís Handbook : The Jargon file : The True Believer : Programming Pearls : The Good Soldier Svejk : The Power Elite

Most popular humor pages:

Manifest of the Softpanorama IT Slacker Society : Ten Commandments of the IT Slackers Society : Computer Humor Collection : BSD Logo Story : The Cuckoo's Egg : IT Slang : C++ Humor : ARE YOU A BBS ADDICT? : The Perl Purity Test : Object oriented programmers of all nations : Financial Humor : Financial Humor Bulletin, 2008 : Financial Humor Bulletin, 2010 : The Most Comprehensive Collection of Editor-related Humor : Programming Language Humor : Goldman Sachs related humor : Greenspan humor : C Humor : Scripting Humor : Real Programmers Humor : Web Humor : GPL-related Humor : OFM Humor : Politically Incorrect Humor : IDS Humor : "Linux Sucks" Humor : Russian Musical Humor : Best Russian Programmer Humor : Microsoft plans to buy Catholic Church : Richard Stallman Related Humor : Admin Humor : Perl-related Humor : Linus Torvalds Related humor : PseudoScience Related Humor : Networking Humor : Shell Humor : Financial Humor Bulletin, 2011 : Financial Humor Bulletin, 2012 : Financial Humor Bulletin, 2013 : Java Humor : Software Engineering Humor : Sun Solaris Related Humor : Education Humor : IBM Humor : Assembler-related Humor : VIM Humor : Computer Viruses Humor : Bright tomorrow is rescheduled to a day after tomorrow : Classic Computer Humor

 

The Last but not Least


Copyright © 1996-2014 by Dr. Nikolai Bezroukov. www.softpanorama.org was created as a service to the UN Sustainable Development Networking Programme (SDNP) in the author free time. This document is an industrial compilation designed and created exclusively for educational use and is distributed under the Softpanorama Content License. Site uses AdSense so you need to be aware of Google privacy policy. Original materials copyright belong to respective owners. Quotes are made for educational purposes only in compliance with the fair use doctrine. This is a Spartan WHYFF (We Help You For Free) site written by people for whom English is not a native language. Grammar and spelling errors should be expected. The site contain some broken links as it develops like a living tree...

You can use PayPal to make a contribution, supporting hosting of this site with different providers to distribute and speed up access. Currently there are two functional mirrors: softpanorama.info (the fastest) and softpanorama.net.

Disclaimer:

The statements, views and opinions presented on this web page are those of the author and are not endorsed by, nor do they necessarily reflect, the opinions of the author present and former employers, SDNP or any other organization the author may be associated with. We do not warrant the correctness of the information provided or its fitness for any purpose.

Last modified: February 19, 2014