Cover V12, i11

Article

nov2003.tar

Do It Yourself with the Shell

Ed Schaefer and Michael Wang

Handy Unix tools such as Perl, awk, sed, and grep are frequently used to solve programming problems, while the shell may be overlooked as a problem-solving tool. Most of us garner Unix knowledge by learning Unix commands and, to the uninitiated, shell programming is just chaining a series of commands together. As a command interpreter, the shell does not distinguish its built-in commands from external commands; you do not call external commands using a function as Perl's system() function. Using the shell over an external tool is analogous to performing a task yourself versus hiring somebody else to do it. Provided you have the knowledge and experience, it's generally better to do it yourself.

This article presents "do it yourself" techniques in shell using real-world examples -- examples gathered from printed material, USENET postings, and our own work. In this article, we contrast standard Unix tool solutions with corresponding shell solutions and compare the different approaches. We present 13 specific sample problems with corresponding solutions within 5 broader technique categories. The techniques considered are: parameter expansion and positional parameters, pattern matching, while-loop, shell arithmetic, and file and process operations.

All of the shell solutions were tested using bash 2.x and ksh93. Most of them work under ksh88 unaltered or with minor modification; however, we did not purposefully twist the code to work under ksh88. Major commercial Unixes (e.g., Solaris, AIX, HP-UX) ship with ksh93. Recent versions of ksh93 and bash are available via open source licenses. When we refer to portability, we mean portability across different platforms, not across different shells.

Technique: Parameter Expansion and Positional Parameters

Problem 1: Find Year, Month, Date from String

Consider the string i="2003-07-04" in the format of YYYY-MM-DD. Find the year, month, and day. Here are two possible awk solutions (Examples 1a and 1b):

set -- $(echo $i | awk '{gsub(/-/, " "); print}')
set -- $(echo $i | awk -F- '{print $1" "$2" "$3}')
# Your Unix variant might require nawk
And, here are two sed solutions (Examples 1c and 1d):

set -- $(echo $i | sed "s/-/ /g")
set -- $(echo $i | sed "s/\([0-9]*\)-\([0-9]*\)-\([0-9]*\)/\1 \2 \3/")
Two possible Perl solutions (Examples 1e and 1f) are as follows:

set -- $(echo $i | perl -pe "s/-/ /g")
set -- $(echo $i | perl -ne "print(STDOUT join(' ', split(/-/)))")
There are also many shell solutions, and we will show four separate ones. The first one (Example 1g) looks like this:

set -- $(IFS=-; echo $i)
This solution is similar to Examples 1b and 1f. Inside, the subshell "$(...)" assigns field separator variable IFS to "-" and expands "$i" into three fields. The built-in set shell assigns the objects into positional parameters $1, $2, and $3, which are year, month, and day.

The use of "subshell $(...)" localizes IFS to the subshell environment, for the purpose of $i expansion. IFS is not affected in the parent shell environment.

echo works in our example because the string in $i is known. In general, it may contain a space or other shell special character. The following shell command takes care of these (Example 1h):

eval set -- $(IFS=-; printf '"%s" ' $i)
However, if this looks complicated, use the more straightforward approach:

oIFS=$IFS
IFS=-
set -- $i
IFS=$oIFS
Here is the second shell solution (Example 1i):

set -- ${i//-/ }
This solution is similar to Examples 1a, 1c, and 1e, which replace every occurrence of "-" with a space, and set the expansion parameters. This solution does not work if $i contains a space. In that case, use:

eval set -- \"${i//-/\" \"}\"
Most of the time, we know our data structure, so the above (complicated) command is not necessary. For clarity, we do not use this general form of command in the rest of article.

The third shell solution (Example 1j) looks like this:

year=${i%%-*}
month=${i#*-}
month=${month%-*}
day=${i##*-}
"%" and "%%" remove the smallest and largest pattern matched from right side, and "#" and "##" remove from the left side. The sed equivalents are as follows (Example 1k):

year=$(echo $i | sed "s:-.*$::")
month=$(echo $i | sed "s:^[^-]*-::")
month=$(echo $month | sed "s:-[^-]*$::")
day=$(echo $i | sed "s:^.*-::")
This method is useful when a variable does not contain a separator. For example, $i is of the form of YYYYMMDD. In this case, we just need to slightly change the shell construct to the following (Example 1l):

year=${i%%????}
month=${i#????}
month=${month%??}
day=${i##??????}
To be more precise, replace "?" with [0-9].

Using the original format, YYYY-MM-DD, the fourth shell solution (Example 1m) is probably the most straightforward solution, if the exact position of the data is known:

year=${i:0:4}
month=${i:5:2}
day=${i:8}
which corresponds to:

year=$(echo $i | cut -c1-4)
month=$(echo $i | cut -c6-7)
day=$(echo $i | cut -c9-)
Problem 1: Discussion

As shown in these examples, the shell provides multiple ways to do the string manipulation. There is no reason to use sed, awk, or Perl to perform this function. The shell solution is preferred for independence and efficiency. We do not need to worry about PATH, tool availability, and tool behavior on different platforms.

Performance can be an issue when using external commands -- especially in a loop. Suppose you need to process a large file, editing data in each line. The time saved using the shell over other tools may be significant.

Problem 2: File System Usage

Parse df -Pk /usr output, and find the usage percentage. The -P implies the POSIX output format. The POSIX-compliant command under Solaris is /usr/xpg4/bin/df. Example 2a shows an awk solution:

# may require nawk under Solaris 7 & SCO Open Server V
df -Pk /usr | awk '!/^Filesystem/ {sub(/%/, ""); print $5}'
Here is a sed solution (Example 2b):

df -Pk /usr | sed -n '/^Filesystem/!s/.* \ \([0-9]\{1,\}\)%.*/\1/p'
We can also solve this with Perl as follows (Example 2c):

df -Pk /usr | perl -pe "BEGIN { $/ = '' } s/.*\s([0-9]+)%\s.*/\$1\n/s"
This solution combines grep, awk, and sed (Example 2d):

df -Pk /usr | grep -v "^Filesystem" | awk '{print $5}' | sed "s:%::"
Note that the two shell techniques introduced in Problem 1 can be applied here. Here is one possible shell solution (Example 2e):

set -- $(df -Pk /usr); echo ${12%\%}
This solution should work on all native Unix systems, but UWIN, a Unix environment running on Microsoft Windows, fails since that file system name has an extra space:

$ df -Pk
Filesystem             Kbytes     Used    Avail Capacity Mounted on
C:\Program Files\UWIN  4000153  3868891   131262     97% /
where C:\Program Files\UWIN is considered as two fields.

A second shell solution (Example 2f) is as follows:

(IFS=%; set -- $(df -Pk /usr); echo ${1##* })
This solution works on both native Unix systems and UWIN. It splits the "df" output by "%", sets the first part to "$1", removes everything to the last space, and leaves the percentage number.

Problem 2: Discussion

Example 2f demonstrates simple techniques on this not-so-obvious case, providing the shortest, quickest, and most elegant solution.

Technique: Pattern Matching

Problem 3: Find Whether $i Is an Integer

To solve this problem, we need to define an integer. If integer definition is 0 or 1 plus or minus signs, followed by a series of digits, the phrase can be directly translated to a shell function (Example 3a):

function is_integer {
  [[ $1 = ?([+-])+([0-9]) ]]
}
Is this function complete? Yes. This function's return code determines whether the condition is true (0) or false (non zero). We can use the function like this (Example 3b):

is_integer $i && echo "yes" || echo "no"
This function thinks example data "-1e2" and "0xff" are not integers. This may be the exact requirement. For example, the script asks the user to input a date in YYYYMMDD format, and checks whether the entered string is an integer before further processing.

However, if decimal and scientific notation are accepted, Bolsky and Korn provide a more generic solution in their book The New KornShell Command and Programming Language (Example 3c):

isnum()
{
    case $1 in
    ( ?([-+])+([0-9])?(.)*([0-9])?([Ee]?([-+])+([0-9])) )
        return 0;;
    ( ?([-+])*([0-9])?(.)+([0-9])?([Ee]?([-+])+([0-9])) )
        return 0;;
    *)  return 1;;
    esac
}
Examples 3a and 3c use extended pattern matching operators, which are not enabled by default in bash. To enable the extended pattern matching, use:

shopt -s extglob
In the rest of article, we assume the extended pattern matching is enabled.

It happens that the external command expr, which evaluates arguments as an expression, only accepts integers as a series of digits. This is a tool solution (Example 3d):

function is_integer {
  expr "$1" + 0 > /dev/null 2>&1
}
However, if expr ever becomes smarter and handles floating-point numbers, scientific notations, and hex numbers, this solution will not work as planned.

We can use regular expressions used by grep -E (Example 3e):

function is_integer {
  printf "%s\n" $1 | grep -E "^[+-]?[0-9]+$"  >/dev/null
}
And, we can use Perl's regular expressions as follows (Examples 3f and 3g):

function is_integer {
  perl -e "\$_ = \"$1\"; /^[+-]?\d+$/ || exit 1"
}

function is_integer {
  printf "%s\n" $1 | perl -e "<> =~ /^[+-]?\d+$/ || exit 1"
}
Problem 3: Discussion

The shell provides a more direct pattern-matching solution than external tools. You must either feed the tool's standard input, or expand it to fit the tool's syntax (Example 3f). Using the shell eliminates the extra process.

Problem 4: Add a Directory in the PATH

In this problem, we need to add a directory in the PATH if it's not already in the PATH (path munge). The /etc/profile that comes with a certain Linux distribution contains this function (Example 4a):

pathmunge () {
        if ! echo $PATH | /bin/egrep -q "(^|:)$1($|:)" ; then
           if [ "$2" = "after" ] ; then
              PATH=$PATH:$1
           else
              PATH=$1:$PATH
           fi
        fi
}
This function checks whether an object already exists in $PATH and, if not, it adds it to the beginning or the ending of the PATH variable, depending on $2. We rewrote this typical pattern-matching exercise as follows (Example 4b):

shopt -s extglob 2>/dev/null
pathmunge() {
  [[ $PATH = ?(*:)$1?(:*) ]] && return 0
  [[ "$2" = "after" ]] && PATH=$PATH:$1 || PATH=$1:$PATH
}
This significant change replaces "/bin/egrep" with pattern matching. The other changes are a matter of style using "&&" and "||" versus if-then-else.

When running each version of the function 10 times in a ksh93 environment, and using $SECONDS for timing (bash does not support floating-point arithmetic), the results are:

pattern version: 0.016 seconds
egrep   version: 0.542 seconds
The egrep version has other problems. The author of this function must have worried about PATH, because the path of egrep is hard-coded. However, egrep is defined in Red Hat 9 as:

#!/bin/sh
exec grep -E ${1+"$@"}
and no path is given to grep. If we use the function to bootstrap PATH:

PATH=
pathmunge /bin
the egrep version fails.

You may think that an easier solution is to use /bin/grep -E directly instead of /bin/egrep. While this works on Linux, it produces an error on Solaris:

/bin/grep: illegal option -- E
Usage: grep -hblcnsviw pattern file . . .
The POSIX version of grep on Solaris resides in /usr/xpg4/bin, not /bin. getconf PATH produces a POSIX-compliant PATH. Since getconf is not built-in on all shell versions, the PATH for getconf consists of all the unique paths on which getconf may reside.

In ksh-style functions that support localized variables, you can do this:

function name {
  typeset PATH=$(PATH=/usr/bin:... getconf PATH)
  ...
}
In POSIX-style functions, do this:

function_name() {
  OPATH=$PATH
  PATH=$(PATH=/usr/bin:... getconf PATH)
  ...
  PATH=$OPATH
}
The extra lines for portability reduce readability and efficiency.

Problem 4: Discussion

A shell program is more portable because it does not depend on external commands; therefore, we need not worry about the PATH definition. Besides portability, there is a cost associated with spawning a process, especially when the process executes often (as in a loop). While it's not always possible to totally avoid external commands, make an effort to use available shell resources.

Technique: While-Loop

Problem 5: Get infile

An Oracle SQL*Loader control file looks like the following:

-- This is comment line: Oracle Sql*Loader reads records in infile
-- ANNOTATIONS.dat, separated by <er>, and loads them into table
-- ANNOTATIONS.
load data
infile 'ANNOTATIONS.dat' "str '<er>'"
into table ANNOTATIONS
...
We want to retrieve the file name "ANNOTATIONS.dat" as part of a larger task. Someone new to shell programming may jump to grep and other tools within easy reach like this (Example 5a):

cat $file | grep -v -- "--" | grep "infile" | awk "{print \$2}" | \
  sed "s:'::g" | head -1
This can also be done without the unnecessary use of cat:

grep -v -- "--" $file | grep "infile" | awk "{print \$2}" | sed \
  "s:'::g" | head -1
But, this solution is inefficient. Six tools are used to solve this problem. A rule of thumb is that when three or more pipes are used, there's an opportunity for optimization. A more efficient one-tool solution using awk might look like this (Example 5b):

awk -F\' "/^infile/ {print \$2; exit}" $control_file
A sed solution looks like (Example 5c):

sed -n "1,/^infile/s/^infile[^']*'\([^']*\)'.*$/\1/p" $control_file
And, here is a Perl solution (Example 5d):

perl -ne "s/^infile[^']*'([^']+)'.*/\$1/ && print && last" $control_file
Our first shell solution (Example 5e) uses a new trick -- read the file line-by-line, and use the previously covered techniques to process the lines.

while read i; do
  [[ $i == infile* ]] && { (IFS="'"; set -- $i; echo $2); break; }
done < $control_file
Here is a second shell solution (Example 5f):

while read i; do
  [[ $i == infile* ]] && { i=${i#*\'}; echo ${i%%\'*}; break; }
done < $control_file
Problem 5: Discussion

This example represents a generic problem. You often must read a file line by line, retrieving some data and making changes to the current line (or the previous or next line). When many files need to be processed, we prefer a shell solution. We processed 450 such files using a for loop:

for ctl in *.ctl
do
  ...
done
On a Sun Fire box with 12 750-MHz CPUs and 12G memory, the timings for the shell solution (Example 5e) were:

real        0.190
user        0.112
sys         0.073
For the awk solution (Example 5b), they were:

real        6.649
user        0.290
sys         1.022
Using the shell makes a real difference!

Problem 6: Skip head and tail

In this problem, we want to print a file ($file), but skip the first "$first" lines and last "$last" lines. In these examples, assume that shell variables "file", "first", and "last" are predefined such as file="/etc/passwd", first=2, and last=3.

Many shell programmers will immediately think of the head and tail commands. Yes indeed, head and tail can do the job. Here is a three-tool solution (Example 6a):

total=$(wc -l < $file)
(( head = total - last ))
(( tail = head - first ))
(( tail > 0 )) && head -$head $file | tail -$tail
The file is read three times. You can also use sed to replace head and tail, and reduce the number of passes. Here is a two-tool solution (Example 6b):

total=$(wc -l < $file)
(( start = first + 1 ))
(( stop = total - last ))
(( total > first + last )) && sed -n "${start},${stop}p" $file
This problem can be solved by sed alone with an advanced sed solution (Example 6c):

sed -ne :a -e "1,$((first + last))!{P;N;D;}" -e "${first}d;N;ba" $file
We can also solve it using the shell alone, without external tools. If we translate the sed solution to shell, it would be (Example 6d):

(( a = 0 ))
set --
while IFS= read -r i; do
  (( a++ ))
  (( a <  first )) && set -- "$@" "$i"
  (( a == first )) && set --
  (( a >  first )) && set -- "$@" "$i"
  (( a >  first + last )) && { printf "%s\n" "$1"; shift; }
done < $file
This solution first reads in lines before the "{first}-th" line. At the "${first}-th" line, it clears the positional parameters (pattern space in sed). Then, it reads and holds the "$last" lines. Finally, it reads in a new line onto the bottom, prints the line on top, and removes it.

However, the shell has its own idioms, and it does not follow sed's idiosyncrasies. Here is a simplified version (Example 6e):

(( a = 0 ))
set --
while IFS= read -r i; do
  (( a++ ))
  (( a >  first )) && set -- "$@" "$i"
  (( a >  first + last )) && { printf "%s\n" "$1"; shift; }
done < $file
We can also use a named array without using shift (Example 6f):

(( a = 0 ))
while IFS= read -r i; do     
  (( a++ ))
  (( b = a % last ))
  (( a > first + last )) && printf "%s\n" "${L[b]}"
  (( a > first )) && L[b]="$i"
done < $file
Problem 6: Discussion

The advantage of this example is its flexibility. The difference between the tool solution and the shell solution is analogous to the SQL language (the tool) and the PL/SQL environment (the shell). With the tool solution, you generally tell the tool what you want to do, but with the shell solution, you build a step-by-step procedure for a solution. Because the work is done step-by-step, it is easy to add new functionality. For example, you can skip certain lines or calculate line lengths with ease.

Example 6c is the fastest when processing a few larger files, and the shell solutions are the fastest when processing many smaller files. If maximum clarity is your goal, then Examples 6a and 6b are easiest to read.

Technique: Shell Arithmetic

Problem 7: What is Next Month

Given a year and month in YYYYMM format, we need to return the next month in the same format. Here is a shell solution (Example 7a):

function next_month {
  typeset ym=$1 y m
  (( y = ym / 100 ))
  (( m = ym - y*100 ))
  (( y += m / 12 ))
  (( m = m % 12 + 1 ))
  printf "%.4d%.2d\n" $y $m
}
This example uses only shell arithmetic. Assume the "next_month" parameter or "ym" is 200307. Since (( y = ym / 100 ) is an integer operation, y is the largest integer not greater than the division, which is the year, 2003 in this case. This is similar to the floor() function in SQL.

(( m = 200307 - 2003*100 )) calculates the month, which is 7. Since (( 7/12 )) is 0, the year is 2003 + 0, which is still 2003. Only when the current month equals 12 is the year incremented by 1.

The modulo "%" operator delivers the remainder. This is similar to the mod() function in SQL. (( 7 % 12 )) is 7, so next month is 7 + 1, which equals 8. When the current month is 12, (( 12 % 12 )) is 0, so next month is 1. The printf command delivers the results in the required format.

If the math makes you queasy, use any combination of shell techniques for "next_month". Without using any external commands, the function can be rewritten as such (Example 7b):

function next_month {
  typeset y=${1%??} m=${1#????}
  (( m += 1 ))
  (( m == 13 )) && { (( m = 1 )); (( y += 1 )); }
  printf "%.4d%.2d\n" $y $m
}
Using expr (Example 7c):

function next_month {
  typeset ym=$1 y m
  y=$(expr $ym / 100)
  m=$(expr $ym - $y \* 100)
  y=$(expr $y + $m / 12)
  m=$(expr $m % 12 + 1)
  printf "%.4d%.2d\n" $y $m
}
Using bc (Example 7d):

function next_month {
  typeset ym=$1 y m
  y=$(print "$ym / 100" | bc)
  m=$(print "$ym - $y*100" | bc)
  y=$(print "$y + $m / 12" | bc)
  m=$(print "$m % 12 + 1" | bc)
  printf "%.4d%.2d\n" $y $m
}
Using GNU date (Example 7e):

GNU_DATE=/path/to/GNU/date
function next_month {
  ${GNU_DATE} -d "${1%??}-${1#????}-01 + 1 month" +%Y%m
}
Problem 7: Discussion

Generally, in the modern shells, there is no reason to use external commands such as expr or bc to perform arithmetic. For complicated date arithmetic, using GNU date or Perl is better than building the logic yourself using shell. Do what you can, but don't hurt yourself.

Problem 8: Sum across Rows

Harry Potter, taking a shell class at Hogwarts School of Witchcraft and Wizardry, posted this problem on comp.unix.shell. He has an input file like the following:

Date,2003-05-01,2003-05-02,2003-05-03,2003-05-04,2003-05-05
item-a,1,1,1,0,3
item-b,2,2,0,0,2
...
He writes, "How can I sum the numbers across each row and add a "total" field to the end of each line?" Example 8a shows an awk solution:

awk -F"," '/^Date/ {print $0 ",total"}
           /^item/ {sum=0; for(i=2;i<=NF;i++){sum+=$i};
                    print $0 "," sum}' input.txt
Our shell translation is (Example 8b):

while IFS= read -r; do
  if [[ $REPLY = "Date"* ]]; then
    echo "$REPLY,total"
  elif [[ $REPLY = "item"* ]]; then
    (( sum = 0 ))
    IFS=","; set -- $REPLY; shift
    for i; do (( sum += i )); done
    echo "$REPLY,$sum"
  fi
done < input.txt
We can take advantage of shell idioms, and shorten the code as follows (Example 8c):

while IFS= read -r a; do
  [[ $a = "Date"* ]] && { echo "$a,total"; continue; }
  [[ $a = "item"* ]] && { 
    b=${a#*,}; eval echo "$a,$(( ${b//,/+} ))"
  }
done < input.txt
The parameter operation ${b//,/+}, available in ksh93 and bash 2.x, is equivalent to $(echo "$b" | sed "s:,:+:g").

Problem 8: Discussion

We compared the performance of Example 3c with that of Example 3a on a 300-MHz Intel box running UWIN. We found that ksh93 performs better for smaller data sets, while awk performs better for larger datasets. At 200 records, ksh93 and awk break even (0.25 seconds). Even for 1000 records, the ksh93 program finishes within a second (0.75 seconds) while awk finishes in 0.35 seconds. Having a pure shell solution -- especially inside a shell program -- may outweigh the sub-second loss for moderate-sized files.

For files with millions of records, however, neither shell nor Unix tools are good solutions. You probably need to load the data into a database, and process it with SQL. Use the right tools for the job.

Technique: File and Process Operations

Problem 9: Move All Files Except "old" to "old" Directory

When applied to this problem, the simple command (Example 9a):

mv * old
produces an error:

mv: old: cannot rename to old/old
You could use grep -v (Example 9b):

ls -1 | grep -v "^old$" | xargs -I {} -t mv {} old
Yes, this works, but a shell pattern can be used for path name expansion. Additionally, pattern matching and substring expansion provide a much simpler solution (Example 9c):

mv !(old) old
This reads as "move everything that is not old to old". On rare occasions, this error may happen:

-ksh: /usr/xpg4/bin/mv: cannot execute [Arg list too long]
This error occurs because ksh calls execve() to start the mv command, and execve() limits its arguments to a size defined by kernel parameter ARG_MAX, which has default value of 1M on Solaris 8 and 128K on Red Hat Linux 9. A shell for loop solves the problem (Example 9d):

for i in !(old); do mv $i old; done
Problem 9: Discussion

Shell patterns can be used in more sophisticated situations. For example, to uncompress all *.gz files except the following:

FRAMEWORK.dat1.gz
VARHISTORY.dat1.gz
VARHISTORY.dat2.gz
SCENARIOS.dat1.gz
SCENARIOS.dat2.gz
SCENARIOS.dat3.gz
you would use:

gzip -d !(FRAMEWORK.dat?|VARHISTORY.dat?|SCENARIOS.dat?).gz
Replace ! with @ if you want only to uncompress these files.

Problem 10: Find Files Created Today

In this problem, we want to find all the files in the current directory created today. (For simplicity, assume there are no subdirectories.) For all solutions, we need to create a reference file (Example 10a):

touch -t $(date +%Y%m%d)0000.00 /tmp/timestamp$$
The following solution uses find (Example 10b):

find * -type f -newer /tmp/timestamp$$ -print
Here is a sed solution (Example 10c):

ls -1rt * /tmp/timestamp11354 | sed "1,/timestamp11354/d"
And here is a shell solution (Example 10d):

for i in *; do [[ $i -nt /tmp/timestamp$$ ]] && echo $i; done
Problem 10: Discussion

We can use a combination of tool and shell solutions to solve problems in more complicated situations. For example:

find . -type f | while read file; do ...; done
As Solaris systems processes are represented in the pseudo file system /proc, we use the shell operator to compare the timing of two Unix processes, like:

[[ /proc/$PID1 -nt /proc/$PID2 ]] && { echo "$PID1 is started after $PID2"; }
Problem 11: Test Whether a File Is Empty

The following example is taken from a magazine published by a commercial database vendor (Example 11a):

#!/bin/ksh
...
v_corruptions="'cat $TMP_FILE | wc -l'"
if [ $v_corruptions -ne 0 ]; then
  echo "Data Block Corruptions Occurred."
fi
Since the shell already provides an operator to test for empty files, the above code can be rewritten as (Example 11a):

[[ -s $TMP_FILE ]] && { echo "Data Block Corruptions Occurred."; }
Besides avoiding starting two commands, the "-s" operator calls the stat() function retrieving information from the stat structure without cycling through the file contents, and, therefore, is more efficient. The ls command does the same, except shell executes an extra fork() function call.

Problem 11: Discussion

This technique may not make a big difference when the $TMP_FILE file is small, but we think it is important to habitually use more efficient solutions.

Problem 12: Check the Existence of a Process

Quite often, we see shell programs like this (Example 12a):

ps -ef | grep ora_smon_$SID | grep -v grep >/dev/null && {
  echo "Instance $SID is up."
}
We can improve the efficiency (Example 12b) as follows:

ps -u$OWNER -f | grep "[o]ra_smon_$SID"
The "-u$OWNER" limits the ps output. The double quotes tell the shell to pass "[o]" unaltered to grep for interpretation. The regular expression "[o]" matches "o", but since the command in the process table is:

grep [o]ra_smon_$SID
it won't match itself. This eliminates the unnecessary grep -v.

Use a more strict expression:

ps -u$OWNER -f | grep "[0-9] ora_smon_$SID"
to make it more unlikely to match itself or other unwanted processes.

Problem 12: Discussion

Some systems (Solaris, Red Hat Linux) provide a pgrep command that "will never consider itself a potential match", according to the man page. Use pgrep if portability is not a concern.

Problem 13: Shut Down the Database

We want to gracefully shut down the Oracle database (shutdown_immediate function). If this does not shut down the database within, say, five minutes, it kills the shutdown process and issues the forceful shutdown command (shutdown_abort):

shutdown_immediate & (( BPID = $! ))
(( SECONDS = 0 ))
while (( SECONDS <= ${MAXTIME:-300} )); do
  [[ -d /proc/$BPID ]] || break
  sleep 1
done
[[ -d /proc/$BPID ]] && kill -TERM $BPID
shutdown_abort
[[ -d /proc/$BPID ]] is more efficient than ps -p $BPID, which potentially executes 300 times inside the while loop. A test on a Sun Ultra 5 (Solaris 8, 360 MHz CPU, 320M memory) shows running the [[ -d /proc/$BPID ]] 300 times takes 0.06 seconds, while running ps p $BPID 300 times takes 34 seconds. The bulk of the time is consumed in fork() calls. The CPU cycles are better used elsewhere.

Additionally, [[ -d /proc/$BPID ]] && kill -TERM $BPID is unlikely to produce an error "kill: $BPID: no such process" than ps -p $BPID && kill -TERM $BPID.

Problem 13: Discussion

In theory, this solution might force a race condition when shutdown_immediate exists, if its PID is reused by another process within a second. In practice, the chance of this happening is nil; however, if it is a concern, check the timestamp and inode number of /proc/$BPID.

Conclusion

Through the problems and solutions presented in this article, we have shown that shell solutions are often more portable, using only the shell, you don't need to worry about various tool availability and behavior. Shell solutions are more efficient -- especially when used within a loop -- and can provide greater flexibility in solving complex problems when no existing tool fits the purpose.

We do not advocate replacing Unix tools with the shell, but encourage you to use tools wisely.

We often see tools overly used when more efficient shell solutions exist. Although it is a path less traveled, the shell offers power and advantages over Unix tools.

Acknowledgements

The comp.unix.shell code snippets from the following individuals are used in our examples: Al Sharka and Dan Mercer (Example 2e), Stephane Chazelas (Example 6c), Charles Demas (Example 8a), Chris F.A. Johnson (Example 8c). We are responsible for any errors in the interpretations and modifications.

References

Bolsky, Morris, David Korn. The New KornShell Command and Programming Language, 1995. Upper Saddle River, NJ: Prentice Hall PTR.

Dougherty, Dale, Arnold Robbins. Sed & Awk, March 1997. Sebastopol, CA: O'Reilly & Associates, Inc.

Wall, Larry, Tom Christiansen, and Jon Orwant. Programming Perl, July 2000. Sebastopol, CA: O'Reilly & Associates, Inc.

Michael Wang earned Master Degrees in Physics (Peking) and Statistics (Columbia). Currently, he is applying his knowledge to Unix systems administration, database management, and programming. He can be reached at: xw73@columbia.edu.

Ed Schaefer is a frequent contributor to Sys Admin. He is a Software Developer and DBA for Intel's Factory Integrated Information Systems, FIIS, in Aloha, OR. Ed also edits the UnixReview.com monthly Shell Corner column. He can be reached at: shellcorner@comcast.net.