<HTML>
<HEAD>
<TITLE>
Jam/MR Language
</TITLE>
</HEAD>
</BODY>
<CENTER>
<a href=http://www.perforce.com/jam/jam.html>
Jam/MR
</a>
<A NAME="TOP">
<H1> The Jam/MR Language
</H1>
</A>
</CENTER>
<P>
This document describes the Jam/MR language, used
to defines rules and dependencies for <b>jam</b>,
the Jam/MR executable program.
For additional information, please see:
<UL>
<LI><a href="Jam.html">The Jam/MR Executable Program</A>
<LI><a href="Jamfile.html">Using Jamfiles and Jambase</A>
<LI><A href="Jambase.html">Jambase Reference</A>
</UL>
<P>
Also, the
<A HREF=http://public.perforce.com/public/jam/src/Jambase>Jambase</a>
source file itself
is an excellent source of Jam/MR language examples.
Jam/MR documentation and source are available from the
<A HREF=http://public.perforce.com/public/index.html>Perforce Public Depot</a>.
<HR>
<H3> Lexical Features
</H3>
<P>
Jam/MR treats its input files as whitespace-separated tokens,
with two exceptions: double quotes (") can enclose whitespace to embed it into a token, and everything between the
matching curly braces ({}) in the definition of a rule
action is treated as a single string. A backslash (\) can
escape a double quote.
<P>
Jam/MR requires whitespace (blanks, tabs, or newlines) to
surround all tokens, including the colon (:) and semicolon
(;) tokens. This is because <b>jam</b> runs on many platforms
and no characters, save whitespace, are uncommon in the
file names on all of those platforms.
<p>
<H4> Reserved Words
</h4>
<P>
Certain tokens are recognized as reserved words, and unless they
are used in expected context they will cause syntax errors.
You can quote reserved words to use them in other contexts.
For example, "switch" is a reserved word, so:
<PRE>
Echo "switch" ;
</pre>
is allowed, but:
<PRE>
Echo switch ;
</PRE>
will get a syntax error.
Words likely to be reserved are any keywords used in
defining rules, flow of control, or variable definition.
<P>
<H3> Targets
</H3>
<P>
The essential Jam/MR data entity is a target.
Built targets are files to be updated. Source targets are the files
used in updating those targets. Pseudotargets are
symbols which represent dependencies on other targets.
Built targets and source targets are collectively referred
to as file targets; pseudotargets are not file targets.
<P>
Each target has a unique identifier, which, in the case
of file targets, is its filename optionally qualified
with grist (a string value used to
assure uniqueness) and/or a pathname,
either rooted or relative to the directory of
<b>jam</b>'s
invocation.
A file target with an identifier of the form
<I>file(member)</I> is an
an ar(1) library member.
<P>
<H3> Rules
</H3>
<P>
Jam/MR's basic language entity is called a rule, which is used
primarily to
relate built targets to their source targets. A rule is defined in two
parts: the Jam/MR statements to execute when the rule
invocation is parsed, and the actions
(shell commands) to execute when updating the built targets
of the rule. A rule may have a procedure definition,
an action definition, or both.
<P>
The Jam/MR statements for defining and invoking rules are as
follows:
<PRE>
rule <I>rulename</I> { <I>statements</I> }
actions [ <I>modifiers</I> ] <I>rulename</I> { <I>commands</I> }
<I>rulename field1 </I>:<I> field2 </I>:<I> ... </I>:<I> fieldN ;</I>
</PRE>
<P>
The first form defines a rule's procedure; the second
defines the rule's updating actions; the third invokes the
rule. Redefining a rule's procedure or actions replaces
the previous definition.
<P>
A rule is invoked with values in <I>field1</I> through
<I>fieldN</I>, which are referenced in the procedure
definition <I>statements</I> as
variables named $(1) through $(<I>N</I>).
A rule defined with a procedure definition and no action
definition is just a procedure call.
<P>
When a rule is invoked, its action definition, if any,
is automatically the first updating action to be associated
with targets.
Any other actions invoked from a rule's procedure definition
<I>statements</I>
will be executing during updating in the order
in which they were invoked.
Updating actions are associated with the targets in <I>field1</I>.
<P>
Built targets and source targets are passed to an updating
action through <I>field1</I> and <I>field2</I>, and
are refenced in the action's <I>commands</I> as
$(1) and $(2) respectively.
$(1) and $(2) may also be referenced as $(<) and $(>)
in action <I>commands</I> and
procedure definition <I>statements</I>.
During updating, <I>commands</I> are passed to the OS, with
$(1) and $(2) replaced by bound targets. (See "Binding"
in the <a href="Jam.html">The Jam/MR Executable Program</A>.)
<P>
<H4> Action Modifiers
</H4>
<P>
The following action <i>modifiers</i> are understood:
<PRE>
actions bind <I>vars</I>
$(vars) will be replaced with bound values.
actions existing
$(>) includes only source targets currently existing.
actions ignore
The return status of the <I>commands</I> is
ignored.
actions piecemeal
<I>commands</I> are repeatedly invoked
with a subset of $(>) small enough to fit in
the command buffer on this OS.
actions quietly
The action is not echoed to the standard
output.
actions together
The $(>) from multiple invocations of the same
action on the same built target are glommed
together.
actions updated
$(>) includes only source targets themselves
marked for updating.
</PRE>
<H4> Built-in Rules
</H4>
<P>
Jam/MR has ten built-in rules, none of which have updating
actions:
<PRE>
ALWAYS <I>targets</I> ;
Rebuilds <I>targets</I>, even if they are up-to-date.
DEPENDS <I>targets1</I> : <I>targets2</I> ;
Makes <I>targets1</I> dependencies of <I>targets2</I>.
ECHO <i>args</I> ;
Blurts out the message <i>args</I> to stdout.
EXIT <i>args</I> ;
Blurts out the message <i>args</I> to stdout and
then exits with a failure status.
INCLUDES <I>targets1</I> : <I>targets2</I> ;
Makes <I>targets2</I> dependencies of anything of which
<I>targets1</I> are dependencies.
LEAVES <I>targets</I> ;
Makes each of <I>targets</I> depend only on its leaf
sources, and not on any intermediate targets. Its leaf
sources are those source targets without any
dependencies themselves.
NOCARE <I>targets</I> ;
Marks <I>targets</I> as possibly being not needed. If a
<I>target</I> is present, its timestamp is used to compute
dependencies; if it is not present, it is not considered a
dependency.
NOTFILE <I>targets</I> ;
Marks <I>targets</I> as pseudotargets, not files.
NOUPDATE <I>targets</I> ;
Causes the timestamps of <I>targets</I> to be ignored:
either <I>target</I> exists or it doesn't. If it exists,
it is considered eternally old.
TEMPORARY <I>targets</I> ;
Marks <I>targets</I> as temporary; causes <I>targets</I> to be removed
when all their dependencies have been updated.
</PRE>
<P>
The ALWAYS, LEAVES, NOCARE, NOTFILE, NOUPDATE, and TEMPORARY affect only the binding phase (q.v.).
<P>
<H3> Flow-of-Control
</H3>
<P>
Jam/MR has several simple flow-of-control statements:
<PRE>
local <i>vars</I> [ = <i>values</i> ] ;
include <I>file</I> ;
for <I>var</I> in <I>list</I> { <I>statements</I> }
switch <I>value</I>
{ case <I>pattern1</I> : <I>statements</I> ;
case <I>pattern2</I> : <I>statements</I> ;
...
}
if <I>cond</I>
{ <I>statements</I> }
[ else { <I>statements</I> } ]
</PRE>
<UL>
<LI>
The local statement instantiates new <i>vars</i> inside
the current {} block. Variables of the same name outside
of the {} block or before the local statement are unaffected
and inaccessible. local may appear anywhere in a block.
The <i>vars</i> are initialized to <i>values</i>, if present.
<P>
<LI>
The include statement causes <b>jam</b> to read
the named <i>file</i>. The file
is bound like regular targets (see Binding, below), but
unlike regular targets the include file cannot be built.
<P>
The include file is inserted into the input stream during
the parsing phase. The primary
input file and all the included file(s) are treated as a
single file; that is, <b>jam</b> infers no scope boundaries
from included files.
<P>
<LI>
The for loop executes <i>statements</i> for each element in
<i>list</i>, setting the variable <i>var</i> to the element value.
<P>
<LI>
The switch statement executes zero or one of the enclosed
<i>statements</i>, depending on which, if any, is the first
case whose <i>pattern</I> matches <i>value</i>. The
<i>pattern</I> values are not variable-expanded.
The <i>pattern</I> values may
include the following wildcards:
<CENTER>
<TABLE>
<TR><TD>
?
<TD> match any single character
<TR><TD>
*
<TD> match zero or more characters
<TR><TD>
[<i>chars</i>]
<TD> match any single character in <i>chars</i>
</TABLE>
</CENTER>
<P>
<LI>
The if statement does the obvious; the else clause is
optional. <i>cond</i> is built of:
<P>
<CENTER>
<TABLE>
<TR><TD>
<CODE><I>a</I></CODE>
<TD> true if any <I>a</I> element is a non-zero-length string
<TR><TD>
<CODE><I>a</I> = <I>b</I></CODE>
<TD> list <I>a</I> matches list <I>b</I> string-for-string
<TR><TD>
<CODE><I>a</I> != <I>b</I> </CODE>
<TD> list <I>a</I> does not match list <I>b</I>
<TR><TD>
<CODE><I>a</I> < <I>b</I> </CODE>
<TD> <I>a[i]</I> string is less than <I>b[i]</I>
string, where <i>i</i> is first mismatched element
in lists <I>a</I> and <I>b</I>
<TR><TD>
<CODE><I>a</I> <= <I>b</I> </CODE>
<TD> every <I>a</I> string is less than or equal to
its <I>b</I> counterpart
<TR><TD>
<CODE><I>a</I> > <I>b</I> </CODE>
<TD> <I>a[i]</I> string is greater than <I>b[i]</I>
string, where <i>i</i> is first mismatched element
<TR><TD>
<CODE><I>a</I> >= <I>b</I> </CODE>
<TD> every <I>a</I> string is greater than or equal to
its <I>b</I> counterpart
<TR><TD>
<CODE><I>a</I> in <I>b</I> </CODE>
<TD> true if all elements of <I>a</I> can be found
in <I>b</I>, or if <I>a</I> has no elements
<TR><TD>
<CODE>! <I>cond</I> </CODE>
<TD> condition not true
<TR><TD>
<CODE><I>cond</I> && <I>cond</I> </CODE>
<TD> conjunction
<TR><TD>
<CODE><I>cond</I> || <I>cond</I> </CODE>
<TD> disjunction
<TR><TD>
<CODE>( <I>cond</I> ) </CODE>
<TD> precedence grouping
</TABLE>
</CENTER>
<P>
</UL>
<H3> Variables
</H3>
<P>
Jam/MR variables are lists of zero or more elements,
with each element being a string value.
An undefined variable is indistinguishable from a
variable with an empty list, however, a defined
variable may have one more elements which are null strings.
<P>
Variables are
either global or target-specific. All variables are referenced as $(VARIABLE).
<P>
A variable is defined with:
<PRE>
<I>variable</I> = <I>elements</I> ;
<I>variable</I> += <I>elements</I> ;
<I>variable</I> on <I>targets</I> = <I>elements</I> ;
<I>variable</I> on <I>targets</I> += <I>elements</I> ;
<I>variable</I> default = <I>elements</I> ;
<I>variable</I> ?= <I>elements</I> ;
</PRE>
<P>
The first two forms set <I>variable</I> globally. The third
and forth forms set a target-specific variable, where
<I>variable</I> takes on a value only during the binding and
updating <I>targets</I>. The = operator replaces any previous
elements of <I>variable</I> with <I>elements</I>; the += operation
adds <I>elements</I> to <I>variable</I>'s list of elements.
The final two forms do the same thing,
set <I>variable</I> globally, but only if it was previously
unset.
<P>
On program start-up, <b>jam</b> imports environment variable
settings into Jam/MR variables. Environment variables are
split at blanks with each word becoming an element in the
variable's list of values. Environment variables whose names
end in PATH are split at $(SPLITPATH) characters (e.g., ":" for
Unix). Environment variable values can be overridden on the command line with the
-s flag.
Jam/MR variables are not re-exported to the shell
that executes the updating actions, but the updating
actions can reference Jam/MR variables with $(<I>variable</I>).
<P>
Variables referenced in updating commands will be replaced
with their values; target-specific values take precedence over global
values. Variables passed as arguments to actions
are replaced with their bound values; the "bind" modifier can be used
on actions to cause other variables to be replaced with bound
values.
<P>
<H4> Variable Expansion
</H4>
<P>
During parsing, Jam/MR performs variable expansion on each
token that is not a keyword or rule name.
Such tokens with embedded variable references are replaced
with zero or more tokens. Variable references are of the
form $(<I>v</I>) or $(<I>vm</I>), where <i>v</i> is the variable
name, and <I>m</I> are optional modifiers.
<P>
Variable expansion in a rule's actions is similar to
variable expansion in statements, except that the action
string is tokenized at whitespace regardless of quoting.
<P>
The result of a token after variable expansion is the
product of the components of the token, where each component is a literal substring or a list substituting a variable reference. For example:
<PRE>
$(X) -> a b c
t$(X) -> ta tb tc
$(X)z -> az bz cz
$(X)-$(X) -> a-a a-b a-c b-a b-b b-c c-a c-b c-c
</PRE>
<P>
The variable name and modifiers can themselves contain a
variable reference, and this partakes of the product as
well:
<PRE>
$(X) -> a b c
$(Y) -> 1 2
$(Z) -> X Y
$($(Z)) -> a b c 1 2
</PRE>
<P>
Because of this product expansion,
if any variable reference in a token is undefined,
the result of the expansion is an empty list.
If any variable element is a null string,
the result propagates the non-null elements:
<PRE>
$(X) -> a ""
$(Y) -> "" 1
$(Z) ->
*$(X)$(Y)* -> *a* *a1* ** *1*
*$(X)$(Z)* ->
</PRE>
<P>
A variable element's string value can be parsed into
grist and filename-related components.
Modifiers to a variable are used to select elements,
select components, and replace components. The modifiers
are:
<CENTER>
<TABLE>
<TR><TD>
[<I>n</I>]
<TD>Select element number <I>n</I> (starting at 1). If
the variable contains fewer than <I>n</I> elements, the
result is a zero-element list.
<TR><TD>
[<I>n</I>-<I>m</I>]
<TD>Select elements number <I>n</I> through <I>m</I>.
<TR><TD>
[<I>n</I>-]
<TD>Select elements number <I>n</I> through the last.
<TR><TD>
:B
<TD>Select filename base.
<TR><TD>
:S
<TD>Select (last) filename suffix.
<TR><TD>
:M
<TD>Select archive member name.
<TR><TD>
:D
<TD>Select directory path.
<TR><TD>
:P
<TD>Select parent directory.
<TR><TD>
:G
<TD>Select grist.
<TR><TD>
:U
<TD>Replace lowercase characters with uppercase.
<TR><TD>
:L
<TD>Replace uppercase characters with lowercase.
<TR><TD>
:<i>chars</I>
<TD>Select the components listed in <i>chars</i>.
<TR><TD>
:G=<I>grist</I>
<TD>Replace grist with <I>grist</I>.
<TR><TD>
:D=<I>path</I>
<TD>Replace directory with <I>path</I>.
<TR><TD>
:B=<I>base</I>
<TD>Replace the base part of file name with <I>base</I>.
<TR><TD>
:S=<I>suf</I>
<TD>Replace the suffix of file name with <I>suf</I>.
<TR><TD>
:M=<I>mem</I>
<TD>Replace the archive member name with <I>mem</I>.
<TR><TD>
:R=<I>root</I>
<TD>Prepend <I>root</I> to the whole file name, if not
already rooted.
<TR><TD>
:X
<TD>Convert suffix character into the file delimiter character.
(Probably doesn't work on MacOS.)
<TR><TD>
:E=<I>regexp</I>
<TD>Apply <I>regexp</I> to the whole file name. Regexp syntax
is essentially similar to that used in Perl.
</TABLE>
</CENTER>
<P>
On VMS, $(var:P) is the parent directory of $(var:D);
on Unix and NT, $(var:P) and $(var:D) are the same.
<P>
<H3> Built-in Rules and Variables
</H3>
This section discusses Jam/MR's built-in rules and variables.
These built-in rules, along with the
other Jam/MR syntax for manipulating variables, provide the
foundation upon which the Jambase is built. A Jamfile, or
(more likely) a Jamrules (q.v.), can make use of these
built-in rules and variables as well.
<P>
<H4> DEPENDS, INCLUDES Rules
</H4>
Two rules build the dependency graph. DEPENDS simply
makes its sources dependencies of its targets. INCLUDES
makes its sources dependencies of anything of which its
targets are dependencies. This reflects the dependencies
that arise when one source file includes another: the
object built from the source file depends both on the
original and included source file, but the two sources
files don't depend on each other. For example:
<PRE>
DEPENDS foo.o : foo.c ;
INCLUDES foo.c : foo.h ;
</PRE>
Both "foo.c" and "foo.h" become dependencies of "foo.o" in
this example.
<P>
<H4> ALWAYS, LEAVES, NOCARE, NOTFILE, NOUPDATE, TEMPORARY Rules
</H4>
Six rules mark targets so that <b>jam</b> treats them differently
during its target binding and updating phase. Normally,
<b>jam</b> updates a target if it is missing, if its filesystem
modification time is older than any of its dependencies
(recursively), or if any of its dependencies are being
updated. This basic behavior can be changed by invoking
the following rules with the target name as the rule's
target:
<P>
The ALWAYS rule causes its targets to be always updated.
This is used for the clean and uninstall targets, as they
have no dependencies and would otherwise appear never to
need building. It is best applied to targets that are
also NOTFILE targets, but it can also be used to force a
real file to be updated as well.
<P>
The NOCARE rule causes <b>jam</b> to ignore its targets if they
can't be found and have no updating actions. Normally,
<b>jam</b> issues a warning about a target that can't be built
and then refuses to build anything that depends on that
target. The HdrRule in Jambase uses NOCARE on the header file names
found during header file scanning, to let <b>jam</b> know that
the included files may not exist. For example, if a
#include is within an #ifdef, the included file may not
actually be around.
<P>
The NOTFILE rule marks its targets as being pseudotargets,
that is, targets that aren't really files. The
actions on such a target are only executed if the target's
dependencies are updated, or if the target is also marked
with ALWAYS. The default <b>jam</b> target "all" is a
pseudotarget. In Jambase, NOTFILE is used to define
several addition convenient pseudotargets.
<P>
The NOUPDATE rule causes <b>jam</b> to ignore the modification
time of the target. This has two effects: first, once
the target has been created it will never be updated; second,
manually updating target will not cause other targets
to be updated. In Jambase, for example, this rule is applied
to directories by the
MkDir rule, because MkDir only cares that the target
directory exists, not when it has last been updated.
<P>
The TEMPORARY rule allows for targets to be deleted after
they are generated. If <b>jam</b> sees that a temporary target
is missing, it will use the target's parent's time when
determining if the target needs updating. Jambase uses
TEMPORARY to mark object files
that are archived in a library after they are built, so
that they can be deleted after they are archived.
<P>
The LEAVES rule makes each of the targets depend only on
its "leaf" dependencies. This makes it immune to its
dependencies being updated, as the "leaf" dependencies are
those without their own dependencies and without updating
actions. This allows a target to be updated only if original
source files change.
<P>
<H4> ECHO, EXIT Rules
</H4>
These two rules are are used only in <b>jam</b>'s parsing phase.
The ECHO rule just echoes its targets to the standard output.
The EXIT rule does the same and then does a brutal,
fatal exit of <b>jam</b>.
<P>
<H4> SEARCH, LOCATE Variables
</H4>
These two variables control the binding of target names to
real files: they indicate what path name is to be
prepended to the target name when substituting values for
variables referenced in action definitions.
(See "Binding" in
<a href="Jam.html">The Jam/MR Executable Program</A>.)
<P>
$(SEARCH) provides a list of directories along which <b>jam</b>
scans looking for a target. $(LOCATE) overrides
$(SEARCH), indicating the directory where the target must
be. Normally, $(SEARCH) is set for existing targets while
$(LOCATE) is set for the targets which <b>jam</b> must build.
Only the first element in $(LOCATE) is used for binding.
If neither $(SEARCH) nor $(LOCATE) are set, or if the name of
the target is a rooted file name (e.g., on UNIX beginning
with "/"), then the file name is assumed to be the target
name.
<P>
Both $(SEARCH) and $(LOCATE) should be set target-specific
and not globally. If they were set globally, <b>jam</b> would
use the same paths for all file binding, which is
not likely to produce sane results.
When writing your own rules, especially ones not built
upon those in Jambase, you may need to set $(SEARCH) or
$(LOCATE) directly.
Almost all of the rules defined in Jambase
set $(SEARCH) and $(LOCATE) to sensible values
for sources they are looking for and targets they create,
respectively.
<P>
<P>
<H4> HDRSCAN, HDRRULE Variables
</H4>
These two variable control header file scanning. The
first is an egrep(1) pattern, with ()'s surrounding the
file name, used to find file inclusion statements in
source files. The second is the name of a rule to invoke
with the results of the scan: the scanned file is the target,
the found files are the sources. This is the only
place where <b>jam</b> invokes a rule through a variable setting.
<P>
Both $(HDRSCAN) and $(HDRRULE) must be set for header file
scanning to take place, and they should be set target-specific
and not globally. If they were set globally, all
files, including executables and libraries, would be
scanned for header file include statements.
<P>
The scanning for header file inclusions is not exact, but
it is at least dynamic, so there is no need to run
something like makedepend(GNU) to create a static dependency
file. The scanning mechanism errs on the side of robustness
(i.e., it is more likely to return filenames that are not actually
used by the compiler than to miss include files)
because it can't tell if #include lines are inside #ifdefs or
other conditional logic.
In Jambase, HdrRule applies the NOCARE rule to each header file
found during scanning so that if the file isn't present and isn't
needed during compilation, <b>jam</b> doesn't skip its dependencies.
<P>
Also, scanning for regular expressions only works
where the included file name is literally in the source
file. It can't handle languages that allow including
files using variable names (as the Jam/MR language itself does).
<P>
<H4> Platform Identifier Variables
</H4>
<P>
A number of Jam/MR built-in variables can be used to
identify runtime platform:
<CENTER>
<TABLE>
<TR><TD>UNIX<TD>true on Unix platforms
<TR><TD>NT<TD>true on NT platforms
<TR><TD>VMS<TD>true on VMS platforms
<TR><TD>MAC<TD>true on MAC platforms
<TR><TD>OS2<TD>true on OS2 platforms
<TR><TD>OS<TD>OS identifier string
<TR><TD>OSVER<TD>OS version, when applicable
<TR><TD>OSPLAT<TD>Underlying architecture, when applicable
<TR><TD>JAMVERSION<TD><b>jam</b> version, currently "2.2"
<TR><TD>JAMUNAME<TD>Ouput of runtime <b>uname</b> command (Unix only)
</TABLE>
</CENTER>
<P>
<H4> JAMDATE Variable
</H4>
Initialized to time and date at <b>jam</b> startup.
<P>
<H4> JAMSHELL Variable
</H4>
When <b>jam</b> executes a rule's action block, it forks and
execs a shell, passing the action block as an argument to
the shell. The invocation of the shell can be controlled by
$(JAMSHELL). On Unix, for example:
<PRE>
JAMSHELL = /bin/sh -c % ;
</PRE>
The % is replaced with the text of the action block.
<P>
<b>jam</b> can build targets in parallel, as long as the
dependencies among files are properly spelled out and
actions don't create fixed named files in the current
directory. (If either of those two provisions are violated,
<B>jam</B> can trip over itself when building in parallel
things which just happen to build OK sequentially.) When
building in parallel, <b>jam</b> simply forks off more than one
shell at a time.
<P>
<b>jam</b> does not directly support building in parallel across
multiple hosts, since that is heavily dependent on the
local environment. To build in parallel across multiple
hosts, you need to write your own shell that provides
access to the multiple hosts. You then reset $(JAMSHELL)
to reference it.
<P>
Just as <b>jam</b> expands a % to be the text of the rule's
action block, it expands a ! to be the multi-process slot
number. The slot number varies between 1 and the number
of concurrent jobs permitted by the -j flag given on the
command line. Armed with this, it is possible to write a
multiple host shell. For example:
<PRE>
#!/bin/sh
# This sample JAMSHELL uses the SunOS on(1) command to execute
# a command string with an identical environment on another host.
#
# Set JAMSHELL = jamshell ! %
#
# where jamshell is the name of this shell file.
#
# This version handles up to -j6; after that they get executed
# locally.
case $1 in
1|4) on winken sh -c "$2";;
2|5) on blinken sh -c "$2";;
3|6) on nod sh -c "$2";;
*) eval "$2";;
esac
</PRE>
<HR>
<A HREF="#TOP">Back to top.</A>
<P>
Copyright 1997, 1999 Perforce Software, Inc.
<BR>
Comments to <A HREF="mailto:info@perforce.com">info@perforce.com</A>
<BR>
Last updated: Jan 27, 1999
</BODY>
</HTML>