#!/usr/bin/perl
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#  mib-t [-Options]... [file]... 
#
# This program chews up symbolic MIBs and produces a C table. The syntax of a
# MIB  file  is  rather  poorly  characterized,  and everyone seems to have a
# different understanding as to  just  what  should  be  there.   This  isn't
# surprising,  as the ASN.1 language in which they are written was written by
# an ISO committee, and those who have the spec generally come to  their  own
# sort of (mis)understanding of what it says.  This program handles what I've
# seen in actual files, and will likely change as I  learn  more  about  this
# arcane subject.
#
# You may name one or more MIB source files on the command line, or feed  the
# source  to  this  program via standard input.  There are a number of output
# files as indicated by the flags.  They are:

	$Dfile = "mibtbl";		# Simple "oid var" output file.
	$Efile = "mib.err";		# Errors and warnings.
	$Ffile = "mibfld.c";		# JC's SNMP "field" table.
	$Gfile = "mibfld.h";		# Defs for mibfld.h
	$Hfile = "mibtbl.h";		# Symbolic codes for variables.
	$Lfile = "mib.log";		# Debug output.
	$Mfile = "snmpd.mib";	# Simplified, regularized MIB.
	$Ofile = "miboid.t";		# MIB OID table.
	$Tfile = "mibtbl.t";		# MIB table (see mibtbl.c).
	$Vfile = "mibvar.t";		# MIB VAR table.
	$Xfile = "mibext.h";		# MIB extern declarations.

# -Cmibtbl.h
#   This writes a list of the "magic" code symbols to "mibtbl.h", in the form
#   of C #define statements.  You may want to edit this file somewhat.
#
# -Emib.err
#   This writes the error messages to "mib.err", which is the default.
#
# -Hmibext.h
#   This says where to write the extern declarations; default is "mibext.h".
#
# -Lmib.log
#   This writes the error messages to "mib.log", which is the default.
#
# -Msnmpd.mib
#   This file receives a copy of the entire MIB, after some editing.  This is
#   the  file  that the various test programs (Get, Next, Walk) read to learn
#   symbolic names for things.
#
# -Omibtbl.h
#   This will write a simple table of numeric and symbolic object identifiers
#   to the file "mibtbl", which is the default name.
#
# -Tmibtbl.t
#   This will write  C  initializers  for  the  VAR  variable[]  array.   The
#   mibview.c  module  #includes "mibtbl.t"; if this program has done its job
#   right, mibview.c should compile without problems.  But you may need to do
#   a small amount of hand tweaking.
#
# Author:  John Chambers [Dec 1992] <jc@trillian.mit.edu> <jc@minya.bcs.org>
#   Basic algorithm written for a contract to Digital Equipment Corporation.
#   Modified extensively on a contract to GTE Government Systems Division.
#   Modified slightly on a contract to GTE Labs.
#
# All the above showed little interest in discussing  whether  I  could  post
# this  on various newsgroups, so there is likely no problem with anyone else
# using it for their own purposes.  You'll almost certainly need to make some
# changes  for  your  own needs.  If you add any interesting features, let me
# know, so I can incorporate them.
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

push(@INC,'sh','../sh');
require "cmagic.pm";
require "cname.pm";
require "oidorder.pm";
require "timewheel.pm";

# Override some perl defaults:
$, = ' ';
$" = '.';

# Here are some patterns that we will use:
$mibvar = '[-_A-Za-z0-9]+';
$psym = '\b([A-Za-z0-9][-+/_A-Za-z0-9]*)\b';	# ASN.1 symbol.
$pnum  = '([0-9]+)';	# Numeric currently means non-negative integer.
$range = '^\.\.+$';		# Two or more dots indicate a range.
$DF = '::=';
$LB = '{';
$RB = '}';
$LP = '(';
$RP = ')';

# For documentation purposes, here's a list of our global tables:
%access   = ();	# Access rights for variables; usually 'RO' or 'NA'.
%children = ();	# Space-separated list of children of a MIB-tree node.
%defval   = ();	# Default values of variables; not often used.
%descr    = ();	# English description of a variable.
%index    = ();	# Gives list of indices for an Entry group.
%indices  = ();	# Gives number of indices for an Entry group.
%min      = ();	# The min value (for INTEGERs) or length (for STRINGs).
%max      = ();	# The max value (for INTEGERs) or length (for STRINGs).
%nlen     = ();	# Length of objectid, including any indices.
%olen     = ();	# Length of objectid, minus any indices.
%onam     = ();	# Translates variable names to objectids.
%cvar     = ();	# Translates objectids to C's symbolic names.
%mvar     = ();	# Translates objectids to MIB symbolic names.
%pad      = ();	# For strings with min length, the pad char.
%parent   = ();	# Name of the next node upward in the MIB tree.
%symnum   = ();	# Translates symbolic INTEGER values to numeric values.
@t        = ();	# Token list; the next N tokens to be processed.
%type     = ();	# The ASN type of a variable.

&inittbl();	# Table initialization.
&cmdline();	# Process the command line.
&file1();	# Initial boilerplating.
&readmib();	# Read the MIB file(s).
&prntmib();	# Produce the output file(s).
&file2();	# Final boilerplating.
exit 0;

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# This registers the read/write permissions for the named variable.  We  only #
# recognize  a  few codes; the ones used by SNMP.  This might be expanded for #
# the local MIB.                                                              #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
sub access {
	local($name,$val) = @_;
	$val =~ s/\s+//g;
	if ($val eq 'read-only') {
		$val = 'RO'
	} elsif ($val eq 'read-write') {
		$val = 'RW'
	} elsif ($val eq 'write-only') {
		$val = 'WO'
	} elsif ($val eq 'not-accessible') {
		$val = 'NA'
	}
	$access{$name} = $val;
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# Add a new oid to the tables; return its value.  As a side-effect, we  write #
# the oid-table line, to either STDOUT or D, whichever is selected.  As a bit #
# of paranoia, we make sure that there's no initial dot in the oid.           #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
sub addoid {
	local($mname,@l) = @_;
	local($cname,$l,$on,$ov);
	($l = "@l") =~ s/^\.*//;		# Dotted-decimal form of name.
	$cname = &cname($mname);		# Convert to C identifier.
	print L "$mname canonicalized to $cname\n" if ($mname ne $cname);
	$ov = $mvar{$l} || '';			# Note old value, if any.
	if ($ov && ($ov ne $cname)) {	# Have we already seen this name?
		print E "$ARGV\[$.\]: Change $l from $mvar{$l} to $cname\n";
	}
	$on = $onam{$cname} || '';		# Note old name, if any.
	if ($on && ($on ne $l)) {		# Has the definition changed?
		print E "Redefine $cname from '$on' to '$l'\n";
	}
	$onam{$cname} = $l;	# Name => oid in dotted-decimal format.
	$onam{$mname} = $l if ($mname ne $cname);
	$cvar{$l} = $cname;		# Oid in dotted-decimal format => C name.
	$mvar{$l} = $mname;		# Oid in dotted-decimal format => MIB name.
	print D "$l\t$cname\n";	# Write OID and name.
	return $l;
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - #
# Return the arg string with all letters capitalized. #
# - - - - - - - - - - - - - - - - - - - - - - - - - - #
sub caps {
	local($x) = @_;
	$x =~ tr/a-z/A-Z/;
	$x;
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# Run thru the command-line args looking for output-file flags. #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
sub cmdline {
	for (@ARGV) {
		if (/^[-+]/) {
			if (/^[-+][Dd](.*)/) {
				$Dfile = $1;
				print "OID   file: $Dfile\n";
			} elsif (/^[-+][Ee](.*)/) {
				$Efile = $1;
				print "Error file: $Efile\n";
			} elsif (/^[-+][Hh](.*)/) {
				$Hfile = $1;
				print "Headerfile: $Hfile\n";
			} elsif (/^[-+][Ll](.*)/) {
				$Lfile = $1;
				print "Log   file: $Lfile\n";
			} elsif (/^[-+][Mm](.*)/) {
				$Mfile = $1;
				print "MIB   file: $Mfile\n";
			} elsif (/^[-+][Oo](.*)/) {
				$Ofile = $1;
				print "Oid   file: $Ofile\n";
			} elsif (/^[-+][Tt](.*)/) {
				$Tfile = $1;
				print "Table file: $Tfile\n";
			} elsif (/^[-+][Vv](.*)/) {
				$Vfile = $1;
				print "Var   file: $Vfile\n";
			} elsif (/^[-+][Xx](.*)/) {
				$Xfile = $1;
				print "Hdr   file: $Xfile\n";
			} else {
				print STDERR "Unknown option \"$_\" ignored.\n";
			}
		} else {
		#	print "\b-\tInput file: $_\n";
			@FILE = (@FILE , $_);
		}
	}
	@ARGV = @FILE;	# Leftovers are input file names.

# Open any of the above files.
	if ($Dfile) {
		open(D,">$Dfile")
			|| die "Can't write to $Dfile [$!]\n";
		select D; $|=1;
	}
	if ($Efile) {
		open(E,">$Efile")
			|| die "Can't write to $Efile [$!]\n";
		select E; $|=1;
	}
	if ($Hfile) {
		open(H,">$Hfile")
			|| die "Can't write to $Hfile [$!]\n";
		select H; $|=1;
	}
	if ($Lfile) {
		open(L,">$Lfile")
			|| die "Can't write to $Lfile [$!]\n";
		select L; $|=1;
	}
	if ($Mfile) {
		open(M,">snmpd.mib")
			|| die "### Can't write to $Mfile [$!]\n";
		select M; $|=1;
	}
	if ($Ofile) {
		open(O,">$Ofile")
			|| die "Can't write to $Ofile [$!]\n";
		select O; $|=1;
	}
	if ($Tfile) {
		open(T,">$Tfile")
			|| die "Can't write to $Tfile [$!]\n";
		select T; $|=1;
	}
	if ($Vfile) {
		open(V,">$Vfile")
			|| die "Can't write to $Vfile [$!]\n";
		select X; $|=1;
	}
	if ($Xfile) {
		open(X,">$Xfile")
			|| die "Can't write to $Xfile [$!]\n";
		select X; $|=1;
	}
	select STDOUT;	# To make debug package work.
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# Given a symbol, canonicalize it.  This can be used to do assorted mapping #
# and renaming.                                                             #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
sub Csym {
	local($sym) = @_;
	$sym =~ s/\s+//g;
	$sym = $sym{$sym} || $sym;
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# This registers a text description for the named  variable.   At #
# present,  we  don't  use such descriptions, but if we need them #
# sometime in the future, we know where to find them.             #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
sub descr {
	local($name,$text) = @_;
	$descr{$name} = $text;
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# Here is where we generate whatever initial boilerplating is needed for  the #
# various output files.  The biggest thing we do here is to read mibfld.c, to #
# extract the names of Fld structs so we can add them to the MIB table.       #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
sub file1 {
	local($f,$l);
	if ($Xfile) {
		print X "#include \"var.h\"\n\n";
	}
	if (open(G,">$Gfile")) {
		print G "#ifndef mibfld_h\n";
		print G "#define mibfld_h\n";
		print G "\n";
		print G "#ifndef   fld_h\n";
		print G "#include \"fld.h\"\n";
		print G "#endif\n";
		print G "\n";
	} else {
		print E "Can't write $Gfile [$!]\n";
		$Gfile = '';	# Suppress later output.
	}
	if (open(F,"<$Ffile")) {
		for $l (<F>) {
			next if ($l =~ /^\*/);
			next if ($l =~ /^\/\*/);
#			print STDOUT "Line: $l";
			if ($l =~ /^#include\s+"G($mibvar)\.f"/) {	# #include "GfooBar.f"
				$f = &cname($1);
				$get{$f} = "&G$f";
				print G "extern Fld G$f;\n" if $Gfile;
				print L "Fld $f is $get{$f}\n";
			} elsif ($l =~ /^#include\s+"S($mibvar)\.f"/) {	# #include "SfooBar.f"
				$f = &cname($1);
				$set{$f} = "&S$f";
				print G "extern Fld S$f;\n" if $Gfile;
				print L "Set $f is $set{$f}\n";
			} elsif ($l =~ /^#define\s*G(\w+)\s*(G$mibvar)/) {	# #define GfooBar GbarFoo
				$f = &cname($1);
				$get{$f} = "&$2";
				print G "#define G$f $2\n" if $Gfile;
				print L "Def $f is $get{$f}\n";
			} elsif ($l =~ /^#define\s*S(\w+)\s*(S$mibvar)/) {	# #define SfooBar SbarFoo
				$f = &cname($1);
				$set{$f} = "&$2";
				print G "#define S$f $2\n" if $Gfile;
				print L "Def $f is $set{$f}\n";
#			} else {
#				print L "Ignore $Ffile: $l";
#				print E "Ignore $Ffile: $l";
			}
		}
		close(F);
	} else {
		print L "### Can't read $Ffile [$!]\n";
		print E "### Can't read $Ffile [$!]\n";
		print G "/* No fld-type variables in this MIB */\n";
	}
	if ($Gfile) {
		print G "\n";
		print G "#endif\n";
		close G;
	}
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# Here is where we generate whatever final boilerplating is  needed  for  the #
# various output files.                                                       #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
sub file2 {
	print M "END\n";
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# Given a string, expand @t until the string appears. The return value is the #
# index where it is found.  The return value is -1 if we hit EOF first.       #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
sub findstr {
	local($s) = @_; local($n);
	while (($n = &havestr($s)) < 0) {
		if (!&tok(@t + 1)) {
			print "EOF\n";
			return -1;
	}	}
	$fs = $n;
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# Get the oid value for a symbolic name, or '*' if it isn't  defined.   As  a #
# kludge,  we  do  a second pass, translating everything to lower case, so we #
# can recognize miscapitalized names. This is time-consuming, but we've had a #
# lot of problems with this, so it's worth the cpu time.                      #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
sub getoid {
	local($n) = @_;
	local($k,$kl,$nl);
	return $onam{$n} if $onam{$n};
	($nl = $n) =~ tr/A-Z/a-z/;
	for $k (keys %onam) {
		($kl = $k) =~ tr/A-Z/a-z/;
		if ($nl eq $kl) {
			print E "$ARGV\[$.\]: Rewrite $n as $k\n";
			return $onam{$k}
		}
	}
	print E "$ARGV\[$.\]: Can't decode '$n'\n";
	return '*';
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# Look thru @t for a string that exactly matches the arg. The return value is #
# the index where it is found.  The return value is 0 if we hit EOF first.    #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
sub havestr {
	local($s) = @_; local($n);
	for $n (0 .. $#t) {
		if ($t[$n] eq $s) {
			return $n;
		}
	}
	-1;
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# This routine handles the IMPORTS clause.  So far, we don't do much with the #
# information,  because  there's  no known way to find the MIB definitions in #
# the FROM clauses.                                                           #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
sub imports {
	local($sym,$mib);

	if (($i = &findstr($RB)) < 0) {
		print E "$ARGV\[$.\]: Can't find '$RB'\n";
		splice(@t,0,1);	# To ensure progress.
		return;
	}
	while (&tok(3)) {
		if ($t[0] eq 'FROM') {
			$mib = $t[1];
			splice(@t,0,2);
		} elsif ($t[1] eq 'FROM') {
			$sym = $t[0];
			$mib = $t[2];
			splice(@t,0,3);
		} elsif ($t[1] eq ',') {
			$sym = $t[0];
			splice(@t,0,2);
		} elsif ($t[0] eq ';') {
			splice(@t,0,1);
			return;
		} elsif ($t[0] eq 'IMPORTS') {
			splice(@t,0,1);
		} else {
			print E "$ARGV\[$.\]: Can'd handle \"$t[0]\"\n";
			splice(@t,0,1);
		}
	}
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# This registers an index for the named variable. This routine should find @t #
# filled with "name , ...  }", where the names are either types or  variables #
# whose  types  can be determined.  All that we actually do is count them, so #
# that we know how many  object-instance  fields  to  expect  for  subsequent #
# variables.                                                                  #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
sub index {
	local($name) = @_;
	local(@list,@new); # local($indx);
	if (($indx = &findstr($RB)) < 0) {
		print E "$ARGV\[$.\]: Can't find matching '$RB'\n";
	} else {
		@new = splice(@t,0,$indx);
		for (@new) {
			@list = (@list , $_)
				if ($_ ne ',');
		}
		$index{$name} = "@list";	# Names/types of indices.
		$indices{$name} = @list;	# Number of indices.
		splice(@t,0,1);		# Get rid of the right brace.
	}
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# Set up some useful tables.  These are mostly string  substitutions,  though #
# any sort of tables may be initialized here.                                 #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
sub inittbl {
#	%code = (	# Names in mibtbl's "magic" field.
#		# No longer anything changed here.
#	);
	%macro = (
#		"AtmAddress", "OCTET STRING (SIZE (0 .. 32))",
	);
	%onam = (	# OID table with name as key.
		'iso', '1',
	);
	%cvar = (	# OID table with value as key.
		'1', 'iso',
	);
	%mvar = (	# OID table with value as key.
		'1', 'iso',
	);
	%sym = (	# Symbolic name simplifications.
		"Digital_Equipment_Corporation", "DEC",
		"GTE_Government_Systems", "GTE",
		"GTE_Laboratories", "GTELabs",
		"International_Business_Machines", "IBM",
		"Microsoft", "MS",
	);
	%typ = (	# Short symbols used in VAR table.
		'INTEGER', 'INT',
		'NETWORKADDRESS', 'ADDR',
		'TIMETICKS', 'TCK',
		'IPADDRESS', 'IPA',
		'STRING', 'STR',
		'SEQUENCE', 'SEQ',
		'Counter', 'CTR',
		'COUNTER', 'CTR',
		'COUNTER64', 'C64',
		'GAUGE', 'GGE',
		'GROUP', 'GRP',
		'FaultString', 'BIN',
	);
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# Canonicalize a line of input.  This is used to iron out some problems  with #
# the way that the MIB is typed by some people. The main thing we look for is #
# the use of unquoted names for integer values.  We might add other sorts  of #
# canonicalization here.  One warning:  This routine must not reduce any line #
# to a null string, since the caller will treat a null line as EOF.           #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
sub inline {
	local ($line);
	$line = <>;
	&timewheel() if ($. % 10 == 0);
	$line =~ s/\s+$/\n/;	# Trim away white space.
	if ($line =~ /^\t+\w.*\s+\(\d+\),*\s*$/) {	# Named-value kludge.
		$line =~ s/^\s*"*/\t\t\t"/;				# Insert initial quote.
		$line =~ s/"*\s*(\(\d+\),*)\s*/"\t$1\n/;	# Insert trailing quote.
	} elsif ($line =~ /^\t+\w.*\s+\(\d+\),*\s*--(.*)$/) {	# Named-value with comment.
		$line =~ s/^\s*"*/\t\t\t"/;				# Insert initial quote.
		$line =~ s/"*\s*(\(\d+\),*)\s*/"\t$1\n/;	# Insert trailing quote.
	}
	$line;	# Make sure the line is returned.
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# This returns a code for the object instance for the name.  The code is '0', #
# 'G',  or  a  string of 'F's, one per object-instance value.  This is rather #
# kludgy, and uses the fact that the INDEX was attached to the  parent  node, #
# not  to  this one.  Also, we look for IpAddress" items, and convert them to #
# four 'F's.                                                                  #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
sub inst {
	local($name) = @_;
	local($inst,$i,@i,$n,@o,$p,$t);
	if ($p = $parent{$name}) {
		@o = split(/\./,$onam{$name});	# Oid list.
		$olen{$name} = @o;
		if ($i = $index{$p}) {
			@i = split(/\./,$i);	# List of instance variables.
			$n = $nlen{$name} = $olen{$name} + @i;	# Length of Oid + instance.
			for (@i) {
				$t = $type{$_};
				if ($type{$_} eq "IpAddress") {
					$inst .= '.F.F.F.F';
					$nlen{$name} += 3;
				} else {
					$inst .= '.F';
				}
			}
			$inst =~ s/^\.//;
		} elsif ($children{$name}) {
			$inst = 'G';
		} else {
			$inst = '0';
			$n = $nlen{$name} = $olen{$name} + 1;	# Length of Oid + instance.
		}
	} else {
		$inst = 'G';	# Root is assumed to be a group.
		$olen{$name} = 1;
	}
	$inst;
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# This  function  takes  a  name  and  a  definition  in  the @t vector.  The #
# definition is scanned, and values are added to the %onam and  %mvar  tables #
# if possible.  There are several formats.                                    #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
sub oid {
	local($name) = @_;
	local($i,$o,$xx);
	if (&findstr($RB) < 0) {
		print E "$ARGV\[$.\]: Incomplete object definition.\n";
		return 0;
	}
	while ($t[0] ne $RB) {
		if (@t > 4 && $t[1] eq $LP && $t[3] eq $RP) {	# name(num)
			$o = &addoid($t[0],$o,$t[2]);
			&parent($xx,$t[0]) if $xx;
			$xx = $t[0];	# Remember last list item.
			splice(@t,0,4);
			next;
		}
		if (@t > 2 && $t[1] > 0) {		# name num
			$o = &addoid($name,&getoid($t[0]),$t[1]);
			&parent($t[0],$name);
			$xx = $t[0];
			splice(@t,0,2);
			next;
		}
		if (@t > 1) { 			# name
			$o .= &getoid($t[0]);
			&parent($xx,$t[0]) if $xx;
			$xx = $t[0];
			splice(@t,0,1);
			next;
		}
		print E "$ARGV\[$.\]: Can't handle OID '@t'\n";
	}
	splice(@t,0,1);	# Delete the right brace.
	0;
}

## - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
## Here's a routine to determine which of two oid values comes first.  This is #
## a bit tricky.  We have to split the oid apart  on  dots,  and  compare  the #
## numbers at corresponding positions until we find a pair that differs.       #
## - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
#sub oidorder {
#	local($i,@a,@b);
#	@a = split(/\./,$a);
#	@b = split(/\./,$b);
##	for ($i=0; $a[$i] && $b[$i]; $i++) {
##		return -1 if ($a[$i] < $b[$i]);
##		return  1 if ($a[$i] > $b[$i]);
##	}
##	return  1 if $a[$i];
##	return -1 if $b[$i];
#	while (@a && @b) {		# From comp.lang.perl
#		return $i if ($i = (shift(@a) <=> shift(@b)));
#	}
#	return -1 if @b;		# b is longer.
#	return  1 if @a;		# a is longer.
#	0;	# They are identical.
#}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# &parent(x,y,...) enters x as the parent of the rest of the args. The parent #
# is also flagged as a group item, overwriting any type it may have had.      #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
sub parent {
	local($p) = shift(@_);
	foreach (@_) {
		$parent{$_} = $p;
		$children{$p} .= $_ . ' ';
	}
	$type{$p} = 'GRP';
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# Dump the MIB table in object-id order. Note that we write to both Hfile and #
# Tfile here, if either is open. We insist on writing the table, to STDOUT if #
# Tfile  isn't  open.   If Hfile isn't open, we don't bother with writing its #
# data (the names of the magic-code symbols).                                 #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
sub prntmib {
	local($a,$d,$c,$f,$m,$n,$O,$t,$V);
	local($cnt,$code,$oid,$type,);
	local($ss,$mm,$hh,$DD,$MM,$YY) = gmtime;
	++$MM;
	printf V
		"static char mib_sccs_id[] = \"@(#)SNMP%s %02d/%02d/%02d %02d:%02d UTC\";\n"
		,$files,$YY,$MM,$DD,$hh,$mm;
	print "\b*\tProduce tables ...\n";
	print V "\n";
	foreach $v (sort(oidorder keys(%mvar))) {
		&timewheel() if ($cnt++ % 10 == 0);
		$m = $mvar{$v};		# MIB's name (with hyphens).
		$n = $cvar{$v};		# C's name (with underscores).
		print L "MIB name \"$m\" is C name \"$n\"\n";
		$i = &inst($m);		# C code for instance(s).
		($oid = "$v.$i") =~ s/\./,/g;
		($code, $c) = &cmagic($m);
		if ($Hfile && $code) {	# Do we need a "magic" code?
			print H "#define $code\t$c\t/* $m */\n"
		}
		$a = $access{$m} || 'XX';
		$d = $descr{$m};
		$P = $pad{$m} || '0';
		$f = ($i eq 'G') ? '0' : 'v_' . $parent{$m};
		$t = ($i eq 'G') ? 'GRP' : &caps($type{$m}) || 'OTHER';
		$type = &type($t);
		$f =~ s/Entry$//;
		$externs{$f} ++;
		$olen = $olen{$m} || 0;
		$nlen = $nlen{$m} || 0;
		$maxnlen = $nlen if $nlen > $maxnlen;	# Note longest OID needed.
		$min  = $min{$m} || 0;
		$max  = $max{$m} || 0;
		$O = &cname("O$n");	# Oid name.
		$V = &cname("V$n");	# Vid name.
		print O "Oid $O\[\]= {";
		print O "$oid";		# Numeric oid value.
		print O ",0};\n";	# Add a null terminator to all entries.
#		print T "\n" if $i eq 'G';
		print X "extern Var $V;\n" if (defined $Xfile);
		print V "Var $V = {$O";
		print V ", $olen";	# Olen (Length of Oid portion)
		print V ", $nlen";	# Nlen (Length including instances */
		print V ", $type";	# Type (short)
		print V ", $min";	# Min val/size.
		print V ", $max";	# Max val/size.
		print V ", \"$m\"";	# Symbolic name
		print V "};\n";
		print T "{&$V";		# Oid pointer
		print T ", $code";	# Magic code number (byte)
		print T ", $min";	# Min val/size.
		print T ", $max";	# Max val/size.
		print T ", $P";		# Pad (byte)
		print T ", $a";		# Access (RO, RW, XX)
		print T ", $f, ";	# Function
		print T ($get{$n} || '0');
		print T ", ";
		print T ($set{$n} || '0');
		print T "},";
#		print T "\n\t/* $d */" if $d;	# Description.
		print T "\n";
	}
	print "\b*\tTables done.\n";
	print H "#define MAXOIDLEN $maxnlen\n"
		if $maxnlen;
	print "\b*\tProduce externs ...\n";
	if (defined $Xfile) {
		for $f (sort keys %externs) {
			&timewheel() if ($cnt++ % 10 == 0);
			print X "extern BP $f();\n"
				if ($f =~ /^[A-Za-z]\w*$/);
		}
	}
	print "\b*\tExterns done.\n";
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# Read the MIB and process each line.  Along the way, we open each file named #
# in  @ARGV,  which  by  now  should contain only names of files that contain #
# ASN.1 source for MIBs.                                                      #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
sub readmib {
	local($m);

	print "\tRead input files ...\n";
	while () {
		&tok(1) || last;	# A few single-token constructs exist.
		if ($t[0] eq ';') {
			splice(@t,0,1);	# Ignore remnant semicolons.
			next;
		}
		if ($t[0] eq "END") {
			printf L "End of \"$mib\"\n";
			printf E "End of \"$mib\"\n";
			splice(@t,0,1);	# END tokens are not really interesting.
			next;
		}
		&tok(2) || last;	# There are lots of two-token constructs.
		if ($t[1] eq 'OBJECT-TYPE') {	# New variable symbol.
			if ($t[0] eq 'IMPORTS') {
				$imports = &findstr(';');
				@imports = splice(@t,0,$imports);
				shift @t;	# Eat up the semicolon.
				next;
			}
			if (($name = $t[0]) !~ $psym) {
				print E "$ARGV\[$.\]: Bad symbol '$t[0]' before $t[1]\n";
				$name = 'UNKNOWN';	# To allow further processing.
			}
			splice(@t,0,2);
			next;
		}
		if ($t[0] eq 'MATCHES' && $t[1] eq 'FOR') {
			splice(@t,0,2);
			$matches = &findstr(';');
			@matches = splice(@t,0,$matches);
			shift @t;	# Eat up the semicolon.
			next;
		}
		if ($t[0] eq 'ACCESS' || $t[0] eq 'MAX-ACCESS') {
			&access($name,$t[1]);
			splice(@t,0,2);
			next;
		}
		if ($t[0] eq 'DESCRIPTION') {
			$descr{$name} = $t[1];	# Descriptions aren't used.
			splice(@t,0,2);
			next;
		}
		if ($t[0] eq 'REFERENCE') {
			$ref{$name} = $t[1];	# References aren't used.
			splice(@t,0,2);
			next;
		}
		if ($t[0] eq 'INDEX' && $t[1] eq $LB) {
			splice(@t,0,2);
			&index($name);
			next;
		}
		if ($t[0] eq 'STATUS') {
			# It's always "mandatory".
			splice(@t,0,2);
			next;
		}
		if ($t[0] eq 'SYNTAX') {
			splice(@t,0,1);
			&syntax($name);
			next;
		}
		if ($t[0] eq $DF && $t[1] eq $LB) {	# "::= { ... }" alone.
			splice(@t,0,2);
			&oid($name);
			next;
		}
		if ($t[0] eq 'DEFVAL' && $t[1] eq $LB) {
			splice(@t,0,2);
			$i = &findstr($RB);	# OID is surrounded by braces.
			@defval = splice(@t,0,$i);
			$defval{$name} = $symnum{$defval[0]} || "@defval";
			shift @t;	# Get rid of the right brace.
			next;
		}
		&tok(3) || last;	# Three-token constructs.
		if ($t[0] eq 'REGISTERED' && $t[1] eq 'AS' && $t[2] eq $LB) {
			splice(@t,0,3);
			$i = &findstr($RB);	# OID is surrounded by braces.
			@o = splice(@t,0,$i);
			$onam{$name} = "@o";
			shift @t;	# Get rid of the right brace.
			next;
		}

#		&tok(4) || last;	# So far, nothing requires 4 tokens.

		&tok(5) || last;	# Lots of constructs require 5 tokens.

		if ($t[1] eq 'OBJECT'
		&&  $t[2] eq 'IDENTIFIER'
		&&  $t[3] eq $DF
		&&  $t[4] eq $LB) {
			$objid = $t[0];
			splice(@t,0,5);
			&oid($objid);
			next;
		}
		if ($t[1] eq 'ATTRIBUTE'
		&&  $t[2] eq 'WITH'
		&&  $t[3] eq 'ATTRIBUTE'
		&&  $t[4] eq 'SYNTAX') {
			$name = $t[0];
			splice(@t,0,5);
			&syntax($name);
			next;
		}
		if ($t[0] eq 'IMPORTS') {
			&imports();
			next;
		}
# We don't actually do anything with "fooEntry ::= SEQUENCE { ... }"
		if ($t[1] eq $DF
		&&  $t[2] eq 'SEQUENCE'
		&&  $t[3] eq $LB) {
			if (($i = &findstr($RB)) < 0) {
				print E "$ARGV\[$.\]: Can't find '$RB'\n";
				splice(@t,0,4);
				next;
			}
			while (@t && $t[0] ne $RB) {
				splice(@t,0,$i + 1);	# Discard them.
			}
			next;
		}
# We note the MIB's name in "<name> DEFINITIONS ::= BEGIN".
		if ($t[1] eq 'DEFINITIONS'
		&&  $t[2] eq $DF
		&&  $t[3] eq 'BEGIN') {
			$mib = $t[0];
			print D "MIB: $mib\n";
			splice(@t,0,4);
			next;
		}
# Handle the utterly flakey syntax that the GTE people insist on using:
#   swclcnFwdSCR(CLP0) OBJECT-TYPE
#   swclcnFwdSCR(CLP0+1) OBJECT-TYPE
		if ($t[1] eq $LP && $t[3] eq $RP && $t[4] eq 'OBJECT-TYPE') {
			$name = $t[0] . '-' . $t[2];
			print E "$ARGV\[$.\]: Change $t[0] $t[1] $t[2] $t[3] to '$name'\n";
			splice(@t,0,4,$name);
			next;
		}
# We try to handle ASN.1 macro definitions here.  The format is
#    <macro> ::= <definition>
# where the <definition> is whatever is on the rest of the line.  It's
# not  at  all  clear  how we should correctly recognize the end of an
# ASN.1 macro definition, given the  standards-be-damned  attitude  of
# the authors of many MIBs.
		if ($t[1] eq $DF) {
			print L "$ARGV\[$.\]: Recognized macro $t[0] $t[1] ...\n";
			$m = $t[0];
			splice(@t,0,2);
			$" = ' '; 
			$macro{$m} = "@t";
			$" = '.';
			print L "$ARGV\[$.\]: Macro \"$m\" is \"$macro{$m}\"\n";
			@t = ();
			next;
		}
		print E "$ARGV\[$.\]: Can't handle token '$t[0]'\n";
		shift @t;
		next;
	}
	print "\b*\tInput done.\n";
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# Process  the SEQUENCE command.  At present, we don't do much with this one, #
# but maybe in the future ...                                                 #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
sub seq {
	local($item);
#	print E "Seq: $SEQUENCE $item\n";
	&getblock($RB);
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# Eat  up input until the next line that ends with the delimiter, and combine #
# it all into one long string.  This is used  primarily  to  read  multi-line #
# quoted strings.  It is also used to locate the final '}' for a SEQUENCE.    #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
sub getblock {
	local($dlm) = @_;
	local($val);
	while (<>) {
		close(ARGV) if eof;
#		s/\s$/ /;
#		s/\s*--\s*.*//;		# Strip off trailing comments.
		if (/(.*)$dlm\s*$/) {	# Look for delimiter plus white stuff.
			$val .= $1;
			return $val;
		}
		$val .= $_;
	}
	print E "$ARGV\[$.\]: EOF in {block}\n";
	$val;
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# This  takes  care of the cruft necessary to keep $Line and $ARGV set to the #
# correct thing for error messages.                                           #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
sub newfile {
	print E "Close $ARGV.\n";
	close(ARGV);
#	$Line = 0;
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# This  registers  the  type  of the named variable.  The type is one or more #
# tokens in @t.                                                               #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
sub syntax {
	local($name) = @_;
	local($type,$i,$qual,@lst);
	$type = shift @t;
	if ($t[0] eq '.') {
		$qual = $type;
		$type = $t[1];
		splice(@t,0,2);
	}
	if ($type =~ /I*NTEGER/i) {
		$type = 'INTEGER';	# Regularize it.
		if ($t[0] eq $LP) {	# INTEGER ( x .. y )
			$i = &findstr($RP);
			&size($i,$type);
		} elsif ($t[0] eq $LB) {	# INTEGER { x .. y }
			$i = &findstr($RB);
			&range($i,$type);
		}
	} elsif ($type eq 'OBJECT' || $type eq 'ObjectId') {
		if ($t[0] eq 'IDENTIFIER') {
			$type = 'OBJID';
			shift @t;
		}
	} elsif (($type =~ /OCTET/i) || ($type =~ /TEXTUAL/i)) {
		if ($t[0] eq 'STRING') {
			$type = 'STRING';
			shift @t;
		}
		if ($t[0] eq $LP) {	# STRING (SIZE x .. y )
			$i = &findstr($RP);
			&size($i,$type);
		}
	} elsif ($type eq 'SINGLE') {
		if (($t[0] eq 'CHARACTER') && ($t[1] eq 'STRING')) {
			$type = 'STRING';
			splice(@t,0,2,'(','SIZE','(','1',')',')');
			&size(6,$type);
			if ($t[0] eq $LP && $t[2] eq '...' && $t[4] eq $RP) {
				@lst = splice(@t,0,5);
			#	$" = ' '; 
				print E "$ARGV\[$.\]: Ignoring",@lst,"\n";
			#	$" = '.';
			}
		} else {
			print E "$ARGV\[$.\]: Can't handle $type $t[0] $t[1]\n";
		}
	} elsif ($type =~ /FaultString/i) {
		print L "$ARGV\[$.\]: Recognized FaultString ...\n";
		$type = 'BIN';
		if ($t[0] eq $LP) {	# FaultString ( x .. y )
			$i = &findstr($RP);
			&size($i,$type);
		}
	} elsif ($type =~ /DisplayString/i) {
		print L "$ARGV\[$.\]: Recognized DisplayString ...\n";
		$type = 'STRING';
		if ($t[0] eq $LP) {	# DisplayString ( x .. y )
			print L "$ARGV\[$.\]: Recognized DisplayString ( ...\n";
			$i = &findstr($RP);
			&size($i,$type);
		}
	} elsif ((($type =~ /Display/i) || ($type =~ /Textual/i)) && ($t[0] =~ /String/i)) {
		print L "$ARGV\[$.\]: Recognized $type String ...\n";
		$type = 'STRING';
		shift @t;
		if ($t[0] eq $LP) {	# DisplayString ( x .. y )
			print L "$ARGV\[$.\]: Recognized Display String ( ...\n";
			$i = &findstr($RP);
			&size($i,$type);
		}
	} elsif ($type eq 'CmotSystemID') {
		$type = 'ARB';
	} elsif ($type eq 'Sequence' || $type eq 'SEQUENCE') {
		if ($t[0] eq 'of' || $t[0] eq 'OF') {
			shift @t;	# Delete the 'of'.
			shift @t;	# Delete the type.
		}
	}
	$type{$name} = $type;
	if ($type eq 'STRING') {
		$pad{$name} = '_';
	} elsif ($type eq 'BIN') {
		$pad{$name} = 0;
	}
fail:
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# The next $len items are a parenthesized size field for the current SYNTAX #
# clause.  We should be called with $t[0] eq $LP.                           #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
sub size {
	local($len,$type) = @_;
	if (($t[2] =~ $range) && ($t[4] eq $RP)) {	# ( M .. N )
		&minval($t[1]);
		&maxval($t[3]);
		splice(@t,0,5); $len -= 5;
	} elsif ($t[1] =~ /SIZE/i) {		# ( SIZE ( N ) )
		print L "$ARGV\[$.\]: Recognized ( SIZE ...\n";
		if  (($t[3] =~ $range) && ($t[5] eq $RP)) {	# ( SIZE M .. N )
			print L "$ARGV\[$.\]: Recognized ( SIZE $t[2] .. $t[4] )\n";
			&minval($t[2]);
			&maxval($t[4]);
			splice(@t,0,6); $len -= 6;
		} elsif (($t[4] =~ $range) && ($t[6] eq $RP) && ($t[7] eq $RP)) {	# ( SIZE ( M .. N ) )
			print L "$ARGV\[$.\]: Recognized ( SIZE ( $t[3] .. $t[5] ) )\n";
			&minval($t[3]);
			&maxval($t[5]);
			splice(@t,0,8); $len -= 8;
		} elsif ($t[2] eq $LP && $t[4] eq $RP && $t[5] eq $RP) {	# ( SIZE ( N ) )
			print L "$ARGV\[$.\]: Recognized ( SIZE ( $t[3] ) )\n";
			&minval($t[3]);
			&maxval($t[3]);
			splice(@t,0,6); $len -= 6;
		} elsif ($t[3] eq $RP) {	# ( SIZE  N  )
			print L "$ARGV\[$.\]: Recognized ( SIZE $t[2] )\n";
			&minval($t[2]);
			&maxval($t[2]);
			splice(@t,0,4); $len -= 4;
		}
	} else {
		print E "$ARGV\[$.\]: Can't handle $t[0] $t[1] $t[2] ...\n";
		splice(@t,0,$len+1) if $len >= 0;	# Discard up to the final paren.
	}
	if ($len > 0) {
		print E "$ARGV\[$.\]: ### size($len,$type) didn't use its args.\n";
		splice(@t,0,$len);
	}
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Record the param as a possible max value for the current $name.
#
sub maxval {
	local($val) = @_;
	if ($max{$name} < $val || !defined($max{$name})) {
		$max{$name} = $val;
	}
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Record the param as a possible min value for the current $name. 
#
sub minval {
	local($val) = @_;
	if ($min{$name} > $val || !defined($min{$name})) {
		$min{$name} = $val;
	}
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# The next $len items are a parenthesized range field for the current  SYNTAX #
# clause.  There are two recognized syntaxes:                                 #
#   SYNTAX INTEGER { 0 ..  1234 }                                             #
# This gives an integer's range.                                              #
#   SYNTAX INTEGER { sym1 (1), sym2 (2) ...  symN (N) }                       #
# This gives a list of legal values, with symbolic (display) names.  We don't #
# actually do anything with the symbolic names; we just note the high and low #
# numeric values.                                                             #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
sub range {
	local($len,$type) = @_;
	if ($t[0] == $LB) {
		shift @t; --$len;
	}
item:
	while ($len > 0 && $t[0] ne $RB) {	# Check out one item in a list.
		if ($t[0] eq $LP) {			# Left paren in first position.
			if ($t[2] eq '..' && $t[4] eq $RP) {	# ( X .. Y )
				&minval($t[1]);
				&maxval($t[3]);
				splice(@t,0,5); $len -= 5;
				next item;
			}
		}
		if ($t[1] eq $LP && $t[3] eq $RP) {		# Symbol ( N )
			$symnum{$t[0]} = $t[2];
			&minval($t[2]);
			&maxval($t[2]);
			splice(@t,0,4); $len -= 4;
			next item;
		}
		if ($t[0] eq ',') {		# Commas are ignored.
			shift @t; --$len;
			next item;
		}
		print E "$ARGV\[$.\]: Ignore '$t[0]'\n";
		shift @t; --$len;	# Ignore one item.
		next item;
#		splice(@t,0,$len);	# Ignore the rest.
#		$len = 0;
	}
	if ($len > 0) {
		print E "$ARGV\[$.\]: ### range($len,$type) didn't use its args.\n";
		splice(@t,0,$len);
	}
	shift @t if $t[0] eq $RB;		# Discard the final right brace.
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# Read in enough to make sure that there at least n tokens in  the  @t  list; #
# return the number of tokens in @t, or zero for failure if we are at EOF and #
# there aren't n tokens left.                                                 #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
sub tok {
	local($n) = @_;
	local($line,@line,$mac,$new,$s,$t);
	while (@t < $n) {
		close(ARGV) if eof;
		($line = &inline())
			|| return 0;		# EOF.
		$new = 1;
		if ($. == 1) {		# First line of new input file.
			print D "File $ARGV:\n";
			print "\b*\tReading $ARGV ...\n";
			$files = "$files $ARGV";	# File list for SCCS id string.
		}
token:	while ($line) {			# Bite off one token from the line.
			$line =~ s/^\s*//;	# Strip off initial white stuff.
			print M $& if $new;	# Preserve initial white stuff.
			if (!$line) {		# Did that empty out the line?
				print M "\n" 	# Preserve line boundaries.
					if !($line || $new);
				last token;		# Get a new line.
			}
#			if ($. == 55 || $. == 56) {
#				print "Line $..\n";
#			}
			if ($line =~ /^\s*--/) {	# Comment.
				print M "\t" if !$new;
				print M $line;
				$line = '';
				last;	# Comments are the rest of the line.
			}
			$new = 0;
			if (substr($line,0,1) eq '"') {	# Quoted string.
				if ($line =~ s/^"([^"]*)"//) {	# Complete on this line.
					$s = $1;
					print M "\"$s\"";
				} elsif ($line =~ s/^"(.*)//) {	# Incomplete.
					$s = $1;
					print M "\"$s\n";
					$s =~ s/\s+$//;
					$s .= ($t = &getblock('"'));
					print M "$t\"";
				}
				@t = (@t , $s);
				next;
			}
			if ($line =~ s/^END\b//) {	# END token.
				print M "-- END\n";		# Avoid multiple ENDs.
				@t = (@t , "END");		# Pass it along to parser.
				next;
			}
			if ($line =~ s/^$psym//) {	# Symbolic name.
				if ($mac = $macro{$1}) {
					print L "$ARGV\[$.\]: Recognized $1\n";
					$line = "$mac $line"; 
					next token;		# Go back and parse the expansion.
				}
				@t = (@t , &Csym($1));
				print M " $1";;
				next;
			}
			if ($line =~ s/^$pnum//) {	# Number.
				@t = (@t , $1);
				print M " $1";;
				next;
			}
			if ($line =~ s/^([(){};,])//) {	# ASN.1 punctuation.
				@t = (@t , $1);
				print M " $1";;
				next;
			}
			if ($line =~ s/^(::=)//) {	# BNF definition.
				@t = (@t , $1);
				print M " $1";;
				next;
			}
			if ($line =~ s/^(\.+)//) {	# String of dots.
				printf L "$ARGV\[$.\]: Recognized %d dots.\n", length($1);
				@t = (@t , $1);
				print M " $1";;
				next;
			}
			if ($line =~ s/^(.)//) {	# Mystery character.
				print L "$ARGV\[$.\]: Unrecognized char \'$1\'\n";
				@t = (@t , $1);
				print M " $1";;
				next;
			}
		}
	}
	return scalar(@t);	# Return number of tokens in token list.
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# Convert an SNMP type to the form used in the agent's variable[] table. This #
# mostly amounts to finding an abbreviation for a longer name.                #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
sub type {
	local($t) = @_;
	$typ{$t} || $t;
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# Given a variable, this returns a numeric value for its  "magic"  code.   At #
# present,  we  just run a counter up.  However, we eventually need to find a #
# better way to handle this, if only because  the  number  is  going  into  a #
# one-byte field in the C MIB-variable table.                                 #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
sub Xcode {
#	local(XSYMBOL,symbol) = @_;
}
