ksbszabo@watvlsi.UUCP (Kevin Szabo) (07/20/85)
I briefly announced this program in net.sources . We have been using the following code for a couple of months now and I find it makes MAN much more useful. Read the enclosed MAN page and find out what the program is all about. Please USE!, your users will like it. You may find that the test directory won't unpack properly on your system. It uses the 4.2 directory flexnames, hence some filenames will be too long. It isn't vital to the distribution. # #---------------------- # Feed to SH in an empty directory. echo x - Makefile sed 's/^X//' >Makefile <<'!E!O!F!' XBIN = /usr/lib XMAN = /usr/man/man8 XCC = cc -O X Xmanalias: manalias.c X $(CC) manalias.c -o manalias X Xinstall: manalias X cp manalias $(BIN) X strip $(BIN)/manalias X Xinstallman: X cp manalias.8 $(MAN) X Xclean: X -rm manalias !E!O!F! echo x - manalias.8 sed 's/^X//' >manalias.8 <<'!E!O!F!' X.TH MANALIAS 8 "30 January 1985" X.SH NAME Xmanalias \- automatically create aliases for man pages X.SH SYNOPSIS X.B manalias X\-nqv <files> X.SH OPTIONS X.IP \-v XVerbose. print out actions on stdout. Quite longwinded. X.IP \-q XDon't quit. Manalias has a threshold of MAX_WARNING messages Xafter which it exits. If -q is specified it continues past the Xthreshold. MAX_WARNINGS is presently 100. X.IP \-p XNo printing. Doesn't babble when it creates an alias Xor finds one which doesn't agree with what it thinks it should be. XThis is overriden by the verbose flag. X.IP \-n XNo action. Prints what it would do instead of doing it. X.SH DESCRIPTION X.B Manalias Xscans unformatted manual pages and creates cross-links Xto all the program or topic names mentioned in the title line of Xthe manual page. These cross-links enable a manual entry to be Xfound using any of the names in the title line. X.B Man(1) Xwill follow ".so manX/manpage" that are found in files, X.B manalias Xwill create files with the ".so" contents by processing the original Xman page. X.B Manalias Xexamines the lines between the first and second X.B .SH Xof each of the file arguments and uses them to create the man aliases. XFor instance, the man page Xbelow will have two aliases created. X.nf X X:::::::::::::: Xman3/sin.3m X:::::::::::::: X\&.TH SIN 3M X\&.SH NAME X\&sin, cos, tan \e\- trigonometric functions X\&.SH SYNOPSIS X\&. X\&. X\&. X XThe aliases created are - X X.ne 10v X:::::::::::::: Xman3/cos.3m X:::::::::::::: X\&.so man3/sin.3m X X:::::::::::::: Xman3/tan.3m X:::::::::::::: X\&.so man3/sin.3m X X.fi X.B Manalias Xrecognizes a comment as something like X.I " \e\- comment " Xor even \fI - comment\fR, and strips them out. Xand strips them out. X.SH MOTIVATION XIt is bloody irritating to ask MAN for a man page, Xhave MAN tell you it isn't there, Xinvoke MAN -k, Xand then have to ask MAN for the real man page. XHopefully this will all go away now. X.SH EXAMPLE XThe program is usually invoked in a form similar to X\fBgetNAME\fR, i.e. you'll need a shell script such as: X.RS X.sp X.nf Xcd /usr/man Xforeach i ( man* ) X cd $i X manalias * |& mail $MAN_MAINTAINER X cd .. Xend X.fi X.RE X.SH AUTHOR XKevin Szabo X.SH FILES X/usr/lib/manalias \- executable X.br X/usr/src/??? \- source X.SH DIAGNOSTICS XErrors and warnings about bad options and unopenable Xfiles go to stderr. Some output goes to Xstdout. These are short reports of files that have the same name as an alias Xfile should have, but have the incorrect contents. The first line of Xthe file is printed, along with what X.B manalias Xthinks it should be. X.B Manalias Xwill also tell you when it creates an alias, and what that alias is. XBoth can be overridden with the \-p flag. XIf a file is encountered that has the correct contents X.B manalias Xis quiet unless verbose is specified, in which case it outputs a message. XWhen verbose and/or no action are specified they send output to stdout. X.SH BUGS XFiles must be in the working directory, Xi.e. pathnames containing '/' are not allowed. X.sp XManalias is quite picky about the form of a X.I ".so pathname" Xsince it does a straight string compare against what it Xwanted to put into the file. Since manalias is the only Xprogram that should have generated the file this is Xnot worth fixing. X.sp X.B Man(1) Xdoesn't like the alias format X.I ".so /absolute/path/name" Xso X.B manalias Xgenerates pathnames relative to /usr/man. X.sp XPeople don't expect a program like X.B manalias Xto have a look at the first lines of their man page, hence X.B manalias Xapplies a few heuristics to create a list of keywords that are Xused as aliases. Keywords are anything up to the first \e or \-. XEverything after that is ignored. Hence imbedded troff commands Xare ignored, plus all the keywords after them. White space (blanks Xand tabs) and commas may separate keywords. Only the tail end Xof a pathname is significant when used as a keyword. Keywords Xare always lowercased and used that way. !E!O!F! echo x - manalias.c sed 's/^X//' >manalias.c <<'!E!O!F!' X/* X * Make Aliases 'links' for man pages. Given a list of man pages X * this program will examine them for a list of names following the X * first .SH macro. For each of these names `manalias' will create X * a file of with the contents '.so $cwd/filename'. MANALIAS X * will check that this file doesn't exist before it attempts to X * create it. X * X * Author: Kevin Szabo, VLSI Group X * University of Waterloo X */ X X#include <stdio.h> X#include <ctype.h> X#include <sys/param.h> X X#if !( defined( SYSIII ) || defined( SYSV ) ) X#define strchr index X#define strrchr rindex X#endif X Xextern char *getwd(); /* Library function definitions */ Xextern char *strrchr(), X *strchr(), X *strcat(); Xextern char *strncpy(), X *strcat(), X *strcpy(); X X#define FATAL 1 X#define WARNING 0 X#define MAX_WARNINGS 100 /* max number of warnings before exit */ X#define MAXKEYLENGTH 300 /* stringlength of string that holds keys */ X X#define FOUND_COMMENT 1 /* results from massage_line */ X#define NO_COMMENT 0 X X#define MAN_DIR "/usr/man/" /* sigh. I wish man was smarter */ X X#define USAGE "usage: %s -v(erbose) -n(o action) -q(uit) -p(rint) files" X Xchar *progname; /* pointer to argv[0] */ Xchar cwd[MAXPATHLEN]; /* current working directory */ X Xint verbose = 0; /* spew my guts out */ Xint noaction = 0; /* just print what was to be done */ Xint noquit = 0; /* don't quit when max-warning exceeded */ Xint noprint = 0; /* don't print what we did */ X X Xmain(argc, argv) Xint argc; Xchar *argv[]; X{ X char *s, *src, *dst; X X progname = argv[0]; X if( getwd( cwd ) == NULL ) X error( FATAL, "can't get working dir: %s", cwd ); X /* X * Okay, kludge time (already you say? The function hasn't started!). X * Man doesn't understand '.so /usr/man/manX/xxx', but it does X * understand '.so manX/xxx'. Thus we look at our current working X * directory, if it is prefixed with /usr/man we delete the prefix. X * Yuck. X */ X if ( 0 == strncmp(cwd,MAN_DIR,strlen(MAN_DIR)) ) { X dst = cwd; X src = cwd + strlen( MAN_DIR ); X while( *src ) { X *dst++ = *src++; X } X *dst = *src; /* copy the null */ X } X X /* parse the option list */ X while( --argc > 0 && (*++argv)[0] == '-' ) X for( s=argv[0]+1; *s != '\0'; s++ ) X switch (*s) { X case 'v': X verbose = 1; X break; X case 'p': X noprint = 1; X break; X case 'n': X noaction = 1; X break; X case 'q': X noquit = 1; X break; X default: X error( FATAL, "unknown option '%.1s'", s ); X } X if ( argc <= 0 ) X error( FATAL, USAGE, progname ); X if ( verbose ) { X printf("%s: current directory is '%s'.\n",progname,cwd); X } X if ( verbose && noprint ) { X error(WARNING,"verbose option overrides noprint",""); X noprint = 0; X } X X /* create aliases for the files left */ X while (argc > 0) { X make_aliases( *argv++ ); X argc--; X } X exit( 0 ); X} X X X X/* X * Given a filename, open it and get information. X * attempt to create files with the contents '.so cwd/filename' as aliases X * for man(1) X */ X Xmake_aliases( filename ) X char *filename; X{ X char headbuf[BUFSIZ]; X char linbuf[BUFSIZ]; X static char keys[MAXKEYLENGTH]; X int keycharleft; X int incomment; X char *slash; X X /* Find if filename refers to sub-directories..we can't handle them */ X slash = strchr( filename, '/' ); X if( slash != NULL ){ X error( WARNING, X "cannot process '%s', files must be in current directory", X filename ); X return; X } X X if (freopen(filename, "r", stdin) == NULL) { X error( WARNING, "could not open '%s' for reading", filename); X return; X } X for (;;) { X if (fgets(headbuf, sizeof headbuf, stdin) == NULL){ X error( WARNING, "file '%s' has no .TH line", filename ); X return; X } X if (headbuf[0] != '.') X continue; X if (headbuf[1] == 'T' && headbuf[2] == 'H') X break; X if (headbuf[1] == 's' && headbuf[2] == 'o'){ X /* skip files with just .so filename */ X if ( verbose ) X printf("%s:file '%s' .so's a file, skipped.\n", X progname, filename ); X return; X } X } X for (;;) { X if (fgets(linbuf, sizeof linbuf, stdin) == NULL){ X error( WARNING, "file '%s' has no .SH line", filename ); X return; X } X if (linbuf[0] != '.') X continue; X if (linbuf[1] == 'S' && linbuf[2] == 'H') X break; X } X /* X * read lines following the `.SH NAME' construct and append X * them to the `keys' string. Stop appending when another .SH X * is found, or if a comment is found ( the \- string ) X */ X X *keys = '\0'; /* null string */ X keycharleft = MAXKEYLENGTH; X for ( incomment=NO_COMMENT; incomment != FOUND_COMMENT;) { X if (fgets(linbuf, sizeof linbuf, stdin) == NULL) { X error( WARNING, "file '%s' has only one .SH line", X filename ); X return; X } X if (linbuf[0] == '.') { X if (linbuf[1] == 'S' && linbuf[2] == 'H') X break; X else X continue; /* skip troff directives */ X } X incomment = massage_line( linbuf ); X keycharleft -= 2 + strlen( linbuf ); /* null+blank=2 chars */ X if( keycharleft > 0 ) { X strcat( keys, linbuf ); X strcat( keys, " " ); X } else { X error( WARNING,"file '%s':too many keywords; ignored", X filename ); X fprintf( stderr,"%s: keys are '%s'.\n", X progname, keys ); X return; X } X } X if ( verbose ) printf("%s: file '%s', keys '%s'\n", X progname, filename, keys ); X X process_keywords( filename, keys ); X} X X/* X * Here we do the work of splitting out the individual keywords X * from the keywords string.The extension that is present on the filename X * is tacked onto each key and we attempt to create an alias by X * that name. X */ X Xprocess_keywords( filename, keywords ) X char *filename, *keywords; X{ X char *basename(); X X char suffix[20]; /* holds the extension of filename (eg. .3m) */ X char cur_token[80]; /* the alias that we are currently processing */ X char *dot; /* pointer to '.' in filename */ X X register char X *token, /* pointer to current keyword */ X *nextoken; /* pointer to next keyword */ X X X dot = strrchr( filename, '.' ); X if ( dot == NULL ) X *suffix = '\0'; X else { X if( strlen( dot ) >= sizeof( suffix ) ) { X error( WARNING,"suffix of '%s' is too long", filename ); X return; X } X strncpy( suffix, dot, sizeof( suffix ) ); X } X suffix[sizeof(suffix)-1] = '\0'; /* ensure string is terminated*/ X X nextoken = keywords; X X while( *nextoken == ' ' ) *nextoken++='\0'; /*strip leading blanks */ X X while( *nextoken != NULL ) { X token = nextoken; X while( *nextoken != NULL && *nextoken != ' ' ) /* skip token */ X nextoken++; X X while( *nextoken == ' ' ) /* null terminate `token' */ X *nextoken++ = '\0'; X X if( (strlen(token)+strlen(suffix)) >= sizeof(cur_token) ) { X error( WARNING,"file '%s': alias is too long",filename); X fprintf( stderr,"%s: %s%s\n", progname, token, suffix ); X continue; X } X strcpy( cur_token, basename(token) ); X X /* malformed pathnames could give us garbage aliases */ X if( strlen( cur_token ) == 0 ) { X if (verbose) printf("%s: alias '%s' skipped.\n", X progname,token); X continue; X } X strcat( cur_token, suffix ); X make_one_alias( filename, cur_token ); X } X} X X/* X * The actual alias is created here. Files are checked for prior X * existance, and junk is printed out if they exist and don't X * contain what we think they should contain. Under no circumstances X * is an already existing file touched/mangled/spindled or mutilated. X * If the alias we are trying to create matches the filename X * we just return, since there is no alias required. Also, if X * through our general mangling we end up with a null file name X * we just return. X */ X Xmake_one_alias( filename, alias ) X char *filename, *alias; X{ X FILE *aliasfile; X char X aliasline[BUFSIZ]; X X if ( verbose ) printf("%s: file '%s' processing alias '%s'.\n",progname, X filename, alias ); X if ( strcmp( filename, alias ) == 0 ) { X if (verbose) printf("%s: alias '%s' skipped.\n",progname,alias); X return; X } X sprintf( aliasline, ".so %s/%s\n" , cwd, filename ); X aliasfile = fopen( alias, "r" ); X reset_sys_error(); X if( aliasfile != NULL ) { X check_alias( alias, aliasfile, aliasline, filename ); X fclose( aliasfile ); X reset_sys_error(); X return; X } X if( noaction ) { X printf("%s: put in file '%s' < %s", progname, alias, aliasline); X } else { X aliasfile = fopen( alias, "w" ); X if ( aliasfile ) { X fputs( aliasline, aliasfile ); X fclose( aliasfile ); X if (!noprint) printf("%s: alias '%s' made for '%s'.\n", X progname, alias, filename ); X } else X error(WARNING,"could not open '%s' for writing", alias); X } X reset_sys_error(); X} X X/* X * Check the existing file's contents against the desired X * contents. If printing or verbose we chatter about what X * we found. X */ X Xcheck_alias( alias, aliasfile, aliasline, filename ) X FILE *aliasfile; X char *alias, *aliasline, *filename; X{ X char linebuf[BUFSIZ]; X int alias_ok; X X if( fgets( linebuf, sizeof(linebuf), aliasfile) == NULL ) { X error( WARNING,"read of alias file '%s' failed", alias); X return; X } X alias_ok = (strcmp(aliasline,linebuf) == 0); X X if ( !alias_ok && !noprint ) { X printf("%s: alias file '%s' exists for manpage '%s'.\n", X progname, alias, filename ); X printf("\tfirst line=%s\tdesired contents=%s", X linebuf, aliasline ); X } X if ( alias_ok && verbose ) { X printf( "%s: alias file '%s' has correct contents\n", X progname, alias ); X } X} X X/* X * Massage line: will convert a string to lower case, convert comma's and X * tabs to spaces, delete trailing newlines, and recognize the ' \- ' X * beginning of comment in a list of names (and delete it). X */ X Xint Xmassage_line( cp ) X register char *cp; X{ X while ( *cp ) { X if( isupper(*cp) ) *cp = tolower(*cp); X if( *cp == ',' || *cp == '\t') *cp = ' '; X if( *cp == '\\' || *cp == '-' ) { X *cp = '\0'; X return( FOUND_COMMENT ); X } X cp++; X } X if (*--cp == '\n') X *cp = 0; X return( NO_COMMENT ); X} X X/* X * basename: return the last part of a full pathname, with X * all the '/dir/' stuff stripped. X */ X char * Xbasename( path ) X char *path; X{ X char *name; X X if ( name=strrchr( path, '/' ) ) X return( name+1 ); X else X return( path ); X} X/* X * Generate an error message. The program name is tacked onto the X * beginning so that a non-interactive user will know where the X * message came from. The system error list is examined if the X * errno variable shows that a system call generated an error. X * Since this variable is not automatically reset we reset it X * after the message is printed. X * Based on routine in Kernighan & Pike X */ Xerror( severity, format, string ) X int severity; X char *format, *string; X{ X extern int errno, sys_nerr; X extern char *sys_errlist[], *progname; X X static warning_count = MAX_WARNINGS; X X if ( progname ) X fprintf( stderr, "%s: ", progname ); X fprintf( stderr, format, string ); X if ( errno > 0 && errno < sys_nerr ) X fprintf( stderr, " (%s)", sys_errlist[errno] ); X fprintf( stderr, ".\n" ); X X if ( severity == FATAL ) X exit( 1 ); X else if( --warning_count <= 0 && !noquit ) { X fprintf( stderr, "%s: Too many warnings, exiting.\n", progname); X exit( 1 ); X } else X errno = 0; /* reset system's error condition */ X} X X/* X * This short routine resets the system error variable so we don't X * falsely include system error messages when something calls error() X * later. This seems kind of kludgey to me, but I am unsure as how X * properly approach the matter. X * X * Possibly I should have two error routines? One that examines the X * system error variable and one that doesn't? I don't know, and I X * don't care any more. X */ Xreset_sys_error() X{ X extern int errno; X X errno = 0; X} !E!O!F! mkdir test cd test echo x - Makefile sed 's/^X//' >Makefile <<'!E!O!F!' Xusage: X @echo "type 'make <printactions>, or <alias> or <verbose> or <clean>'" X Xprintactions: X ../manalias -n * X Xverbose: X ../manalias -vn * X Xalias: X ../manalias * X X# don't change the *.* notation otherwise you will delete the X# Makefile! Xclean: X -rm -f `grep -l '\.so' *.*` !E!O!F! echo x - badalias.1 sed 's/^X//' >badalias.1 <<'!E!O!F!' XThis isn't the line that Manalias wants. !E!O!F! echo x - extraTroffDirectives.pqr sed 's/^X//' >extraTroffDirectives.pqr <<'!E!O!F!' X.TH STRING 3 "19 January 1983" X.UC 4 X.SH NAME X.bp Xextrajunk / \- X.nh X.SH SYNOPSIS !E!O!F! echo x - noSH.7 sed 's/^X//' >noSH.7 <<'!E!O!F!' X X.TH STRING 3 "19 January 1983" X.UC 4 !E!O!F! echo x - noTH.7 sed 's/^X//' >noTH.7 <<'!E!O!F!' X..TH STRING 3 "19 January 1983" X.UC 4 X.SH Xjunk one two three X.SH !E!O!F! echo x - oneSH.7 sed 's/^X//' >oneSH.7 <<'!E!O!F!' X.TH STRING 3 "19 January 1983" X.UC 4 X.SH Xwe only have one sh line !E!O!F! echo x - pagewithbadalias.1 sed 's/^X//' >pagewithbadalias.1 <<'!E!O!F!' X.TH STRING 3 "19 January 1983" X.UC 4 X.SH NAME Xbadalias \- string operations X.SH SYNOPSIS !E!O!F! echo x - string.3 sed 's/^X//' >string.3 <<'!E!O!F!' X.TH STRING 3 "19 January 1983" X.UC 4 X.SH NAME Xstrcat, strncat, strcmp, strncmp, strcpy, strncpy, strlen, index, rindex \- string operations X.SH SYNOPSIS X.nf X.B #include <strings.h> X.PP X.B char *strcat(s1, s2) X.B char *s1, *s2; X.PP X.B char *strncat(s1, s2, n) X.B char *s1, *s2; X.PP X.B strcmp(s1, s2) X.B char *s1, *s2; X.PP X.B strncmp(s1, s2, n) X.B char *s1, *s2; X.PP X.B char *strcpy(s1, s2) X.B char *s1, *s2; X.PP X.B char *strncpy(s1, s2, n) X.B char *s1, *s2; X.PP X.B strlen(s) X.B char *s; X.PP X.B char *index(s, c) X.B char *s, c; X.PP X.B char *rindex(s, c) X.B char *s, c; X.fi X.SH DESCRIPTION XThese functions operate on null-terminated strings. XThey do not check for overflow of any receiving string. X.PP X.I Strcat Xappends a copy of string X.I s2 Xto the end of string X.IR s1 . X.I Strncat Xcopies at most X.I n Xcharacters. Both return a pointer to the null-terminated result. X.PP X.I Strcmp Xcompares its arguments and returns an integer Xgreater than, equal to, or less than 0, according as X.I s1 Xis lexicographically greater than, equal to, or less than X.IR s2 . X.I Strncmp Xmakes the same comparison but looks at at most X.I n Xcharacters. X.PP X.I Strcpy Xcopies string X.I s2 Xto X.I s1, Xstopping after the null character has been moved. X.I Strncpy Xcopies exactly X.I n Xcharacters, truncating or null-padding X.I s2; Xthe target may not be null-terminated if the length of X.I s2 Xis X.I n Xor more. Both return X.IR s1 . X.PP X.I Strlen Xreturns the number of non-null characters in X.IR s . X.PP X.I Index X.RI ( rindex ) Xreturns a pointer to the first (last) occurrence of character X.I c Xin string X.I s, Xor zero if X.I c Xdoes not occur in the string. !E!O!F! echo x - string.badextensionwhichistoolong sed 's/^X//' >string.badextensionwhichistoolong <<'!E!O!F!' X.TH STRING 3 "19 January 1983" X.UC 4 X.SH NAME Xstrcat, strncat, strcmp, strncmp, strcpy, strncpy, strlen, index, rindex \- string operations X.SH SYNOPSIS X.nf X.B #include <strings.h> X.PP X.B char *strcat(s1, s2) X.B char *s1, *s2; X.PP X.B char *strncat(s1, s2, n) X.B char *s1, *s2; X.PP X.B strcmp(s1, s2) X.B char *s1, *s2; X.PP X.B strncmp(s1, s2, n) X.B char *s1, *s2; X.PP X.B char *strcpy(s1, s2) X.B char *s1, *s2; X.PP X.B char *strncpy(s1, s2, n) X.B char *s1, *s2; X.PP X.B strlen(s) X.B char *s; X.PP X.B char *index(s, c) X.B char *s, c; X.PP X.B char *rindex(s, c) X.B char *s, c; X.fi X.SH DESCRIPTION XThese functions operate on null-terminated strings. XThey do not check for overflow of any receiving string. X.PP X.I Strcat Xappends a copy of string X.I s2 Xto the end of string X.IR s1 . X.I Strncat Xcopies at most X.I n Xcharacters. Both return a pointer to the null-terminated result. X.PP X.I Strcmp Xcompares its arguments and returns an integer Xgreater than, equal to, or less than 0, according as X.I s1 Xis lexicographically greater than, equal to, or less than X.IR s2 . X.I Strncmp Xmakes the same comparison but looks at at most X.I n Xcharacters. X.PP X.I Strcpy Xcopies string X.I s2 Xto X.I s1, Xstopping after the null character has been moved. X.I Strncpy Xcopies exactly X.I n Xcharacters, truncating or null-padding X.I s2; Xthe target may not be null-terminated if the length of X.I s2 Xis X.I n Xor more. Both return X.IR s1 . X.PP X.I Strlen Xreturns the number of non-null characters in X.IR s . X.PP X.I Index X.RI ( rindex ) Xreturns a pointer to the first (last) occurrence of character X.I c Xin string X.I s, Xor zero if X.I c Xdoes not occur in the string. !E!O!F! echo x - toomanykeywords.4w sed 's/^X//' >toomanykeywords.4w <<'!E!O!F!' X.TH STRING 3 "19 January 1983" X.UC 4 X.SH NAME Xstrcat, strncat, strcmp, strncmp, strcpy, strncpy, strlen, index, rindex XThese functions operate on nullterminated strings. XThey do not check for overflow of any receiving string. X.PP X.I Strcat Xappends a copy of string X.I s2 Xto the end of string X.IR s1 . X.I Strncat Xcopies at most X.I n Xcharacters. Both return a pointer to the nullterminated result. X.PP X.I Strcmp Xcompares its arguments and returns an integer Xgreater than, equal to, or less than 0, according as X.I s1 Xis lexicographically greater than, equal to, or less than X.IR s2 . X.I Strncmp Xmakes the same comparison but looks at at most X.I n Xcharacters. X.PP X.I Strcpy Xcopies string X.I s2 Xto X.I s1, Xstopping after the null character has been moved. X.I Strncpy Xcopies exactly X.I n Xcharacters, truncating or nullpadding X.I s2; Xthe target may not be nullterminated if the length of X.I s2 Xis X.I n Xor more. Both return X.IR s1 . X.PP X.I Strlen Xreturns the number of nonnull characters in X.IR s . X.PP X.I Index X.RI ( rindex ) Xreturns a pointer to the first (last) occurrence of character X.I c Xin string X.I s, Xor zero if X.I c Xdoes not occur in the string. !E!O!F! cd .. cd .. exit -- Kevin Szabo' watmath!wateng!ksbszabo (U of W VLSI Group, Waterloo, Ont, Canada)