home *** CD-ROM | disk | FTP | other *** search
/ Skunkware 98 / Skunkware 98.iso / osr5 / sco / scripts / www / httplog < prev    next >
Encoding:
AWK Script  |  1997-08-26  |  111.1 KB  |  3,113 lines

  1. #!/usr/local/bin/gawk -f
  2. #!/usr/bin/awk -f
  3. # @(#) httplog.gawk 3.1 97/04/30
  4. # 94/07/18 John H. DuBois III (john@armory.com)  (based on ftplog program)
  5. # 94/08 - 94/09 Added r option.
  6. # 94/09/23 Added Total line for processed reports, and adRnONq options.
  7. # 94/10/03 Added L option.
  8. # 94/11/02 Fixed d option.
  9. # 94/12/26 Do progress dots only if they will go to a tty.
  10. # 95/03/24 Added support for new httpd log format.
  11. # 95/04/15 Turn on -R automatically.  Added yiTD options.
  12. # 95/07/19 Added S option.
  13. # 96/01/27 Added rcfile processing, f pseudo-option
  14. # 96/01/29 Create entire mail message so that Content-type header can be added.
  15. # 96/02/10 Added X-HTTPReport-Levels header and u option.
  16. # 96/05/09 Make mail subject be a single word (HTTP_Report) so that it can be
  17. #          more easily selected from a .maildelivery file
  18. # 96/07/20 Added g option and tag processing.
  19. # 96/10/12 Make t option work again.
  20. # 97/02/04 Skip failed requests.  Read standard input if logfiles named.
  21. #          Added ABekKI options.  Let record number be given with -x.
  22. # 97/02/11 Print full line num instead of last digit for progress indication.
  23. # 97/02/17 Report # of records skipped.  Added "allhits" user report option.
  24. #          Added C option.  Convert control chars to symbolic form in lots of
  25. #          stuff read from logfile & printed by debug code.
  26. # 97/02/28 Added U option.
  27. # 97/03/20 Added Y option.
  28. # 97/04/09 Fixed header printing.  Do not convert + to space in URLs.
  29. # 97/04/20 Added E option.
  30. # 97/04/30 Fixed E option.
  31.  
  32. BEGIN {
  33.     Setup()
  34.     # Need to allow >1 space for reprocessing output
  35.     FS = "[ ]+"
  36. }
  37.  
  38. # Global vars initialized in Setup():
  39. # Name        Name of this program.
  40. # Width        Screen width.
  41. # Options[]    Options set from command line.
  42. # Debug        Whether debugging is turned on.
  43. # Truncate    Whether to truncate objects to 1st path component.
  44. # MostAccessed    Show most accessed page under each dir.
  45. # Reprocess    Process output of previous httplog runs.
  46. # IGNORECASE    Set to 1 so that case is ignored in pattern matches.
  47. # Progress    Whether progress characters should be printed.
  48. # Subject    Subject of mail sent to users (not yet used).
  49. # ObjPattern    Pattern that objects must match.
  50. # HostPattern    Pattern that host names must match.
  51. # NotObjPattern    Pattern that objects must not match.
  52. # NotHostPattern    Pattern that host names must not match.
  53. # TrackObjects    Track object accesses.
  54. # TrackHosts    Track hosts.  Only one of TrackObjects or TrackHosts may be on.
  55. # HTMLout    Produce HTML-formatted output.
  56. # Interval    Counting interval.
  57. # Counting    Doing access counting.
  58. # CountHosts    Report the number of different hosts that accessed each object,
  59. #               rather than the total number of accesses of each object.
  60. # UseSubmit    Use submit to inject mail into the queue.
  61. # Setup() also calls MailRepSetup(), which sets additional vars.
  62.  
  63. function Setup(  RUsage,Usage,ConfigFile,rcFile,DefLevel,Tag) {
  64.     
  65.     Name = "httplog"
  66.     rcFile = "." Name
  67.     Width = 79
  68.     Sender = "www"
  69.     Subject = "HTTP_Report"            # Subject of mail sent to users
  70.     RUsage = \
  71. Name " -r [-dtPU] [-l<default-report-level>] [-c<config-file>] [-s<subject>]\n"\
  72. "   [-u<hostname>] [-L<level-set>] [-M<mail-address>] [-w<width>] [-g<tag>]\n"\
  73. "   [-x[rec#:]level] [logfile ...]"
  74.     Usage = "Usage:\n" \
  75. Name " [-aAbCdDeIkKpqhRHyS] [-[oO]<object-pat>] [-[nN]<hostname-pat>]\n"\
  76. "   [-w<width>] [-u<hostname>] [-i<interval>] [-T<periods>] [-x[rec#:]level]\n"\
  77. "   [logfile ...]\n" RUsage
  78.     # Can't use gawk flags fFmWv.  f is a pseudo-opt for FILES.
  79.     # iTD are the interval graphing options.
  80.     # rPMstlLcY are the report-all options.
  81.     # adpqyR are the regular-report-only options.
  82.     # Unused option letters: GjJQVXzZ
  83.     ARGC = \
  84.     Opts(Name,Usage,"aAbBc;dDeE;f;i;kKl:L;n;N;o;O;pPqs:ST)u;Uw<yYCg:hHIM;rRtx;",
  85.     0,"~/" rcFile ":$UHOME/" rcFile ":/etc/default/httplog",
  86.     "MOSTACCESSED,ALLACCESSES,NOHEADER,LOGBYTES,MAPFILE,TOPLEVEL,BYDAY,"\
  87.     "LOGFAILED,MAILFROM,FILES,INTERVAL,NOTRACKLAST,BYTESORT,DEFLEVEL,LEVELS,"\
  88.     "HOSTPAT,NOTHOSTPAT,OBJECT,NOTOBJECT,NOPROGRESS,PROGRESS,BYHOST,SUBJECT,"\
  89.     "NUMHOST,PERIODS,HOSTNAME,CONGLOMUSERS,WIDTH,HTML,USESUBMIT",
  90.     0,"",0,"","a,d,q,y,C;o,O;n,N;rPMstlLc,adpqyR,iTD")
  91.  
  92.     # The following are used only for allreport mode
  93.     ConfigFile = "/etc/default/httpreport"    # Main config file
  94.     userMapfile = ".httpreport"            # Per-user report config file
  95.     DefLevel = "none"    # Default report level for users who have no rcfile
  96.     # The valid report levels, and the order they will appear in reports
  97.  
  98.     if ("h" in Options) {
  99.     printf \
  100. "%s: Generate access report from the httpd logfile.\n"\
  101. "%s\n"\
  102. "For each object accessed, a line is printed detailing the number of times\n"\
  103. "it was accessed, the time of the last access, the remote host that the\n"\
  104. "access was made from, and the object accessed.  Output is sorted by the\n"\
  105. "number of times each file was accessed.  If no logfiles are given, one or\n"\
  106. "more default logfiles are read if they have been defined.  The default\n"\
  107. "logfile can be set by assigning a value to the variable FILES in one of\n"\
  108. "the configuration files described below.  The value should be a\n"\
  109. "comma-separated list of files to process.\n"\
  110. "Options:\n"\
  111. "Some of the following options can also be set by assigning values to\n"\
  112. "variables in a configuration file.  Three configuration files are read, in\n"\
  113. "order: a file named %s in the invoking user's home directory; a file\n"\
  114. "named %s in the directory specified by the environment variable UHOME\n"\
  115. "(if it is set); and the file /etc/default/httplog.  Variables are assigned\n"\
  116. "to with the syntax:  varname=value  or in the case of flags, by simply\n"\
  117. "putting the indicated variable name in the file without a value.\n"\
  118. "A variable assigned to in one of these files will override values assigned\n"\
  119. "to the same variable in one of the files read after it.  To turn off an\n"\
  120. "option and prevent it from being set in a file read later, assign it a\n"\
  121. "value of 0.  e.g. if BYHOST is set in /etc/default/httplog, BYHOST=0 in\n"\
  122. "a %s file will override it.  Flag options can be turned off on the\n"\
  123. "command line by following them immediately with '-', e.g. -n- to turn off\n"\
  124. "the n option in such a way that it cannot be turned on in a config file.\n"\
  125. "Variable names appear in parentheses in the option descriptions.\n"\
  126. "-a: For each first-directory-component that is part of any object, display\n"\
  127. "    only the most-accessed object that appears below it. (MOSTACCESSED)\n"\
  128. "-d: Truncate objects to their first directory component before processing.\n"\
  129. "    Objects that begin with /~ and do not have any further directory\n"\
  130. "    components are grouped under the listing for '/~'.  Other Objects that\n"\
  131. "    do not have at least one directory component are grouped under the\n"\
  132. "    listing for '/'.  (TOPLEVEL)\n"\
  133. "-C: Generate total counts (accesses and bytes transferred) only, without\n"\
  134. "    generating a per-URL report.  This can also be used with -x1 as a\n"\
  135. "    fast(er) check of logfiles for errors.\n"\
  136. "-I: Read the standard input.  This can also be specified by giving a\n"\
  137. "    filename of /dev/stdin.\n"\
  138. "-[oO]<object-pat>: Process only objects that match (o) or do not match (O)\n"\
  139. "    pattern <object-pat>. (OBJECT, NOTOBJECT)\n"\
  140. "-[nN]<hostname-pat>: Process only objects accessed from a host whose name\n"\
  141. "    matches (n) or does not match (N) <hostname-pat>. (HOSTPAT, NOTHOSTPAT)\n"\
  142. "    The patterns given with [oOnN] are not implicitely anchored at the\n"\
  143. "    beginning or end.\n"\
  144. "-h: Print this help.\n"\
  145. "-b: Do not print a header.  (NOHEADER)\n"\
  146. "-e: Count failed HTTP requests (those with HTTP response codes of 4xx and\n"\
  147. "    5xx).  Normally, they are skipped.  (LOGFAILED)\n"\
  148. "-R: Reprocess the output of previous runs of %s.  In order for dates to be\n"\
  149. "    interpreted correctly, if more than one old run is processed, they\n"\
  150. "    should be given in order of the time periods they cover.  %s\n"\
  151. "    will normally automatically recognize its output and turn on the -R\n"\
  152. "    option; an explicit -R is only needed if the output header has been\n"\
  153. "    modified or removed.\n"\
  154. "-p: Do not print progress numbers when reading logfile. (NOPROGRESS)\n"\
  155. "-S: Report the number of different hosts that accessed each object, or if\n"\
  156. "    -q is also given, the number different objects accessed by each host,\n"\
  157. "    rather than the total number of accesses of each object or host.\n"\
  158. "    (NUMHOST)\n"\
  159. "-q: Track requesting hostnames rather than objects.  (BYHOST)\n"\
  160. "-k: Do not keep track of the time of last access and the last host that\n"\
  161. "    accessed an object (or, if -q is given, the last object accessed by a\n"\
  162. "    host).  (NOTRACKLAST)\n"\
  163. "-A: Generate a report that, for each object, includes a time and hostname\n"\
  164. "    for each access.  (ALLACCESSES)\n"\
  165. "-B: Record & display the total number of bytes transferred for each URL\n"\
  166. "    or, if -q is given, host.  This option is only available if the\n"\
  167. "    neccessary informatio is recorded in the logfile.  (LOGBYTES)\n"\
  168. "-K: Sort output by bytes transferred instead of access count.  (SORTBYTES)\n"\
  169. "-w<width>: Set the screen width to <width>.  The host field is truncated\n"\
  170. "    if neccessary to make the line <width> or fewer characters long.  Use\n"\
  171. "    0 to prevent any truncation.  The default is %d.  (WIDTH)\n"\
  172. "-r: Run reports for all users.  Use -H for a description of this mode.\n"\
  173. "-x<[record-number:]level>: Turn on debugging at level <level>, which should\n"\
  174. "    be an integer greater than 0.  If record-number is given, debugging at\n"\
  175. "    the given level is not turned on until record the given record number\n"\
  176. "    is reached.  Example: -x59500:10\n"\
  177. "-y: Produce HTML output.  Items in the Object field get hrefs, etc. (HTML)\n"\
  178. "-u: Set the hostname used in generating the URLs for the -y option, and\n"\
  179. "    for generating HTML reports requested by users when -r is used.  By\n"\
  180. "    default, the network hostname of the local system is used. (HOSTNAME)\n"\
  181. "\n"\
  182. "The rest of the options are used for generating reports on the total\n"\
  183. "number of accesses over various periods.  The accesses counted may be\n"\
  184. "restricted by the -[oOnN] options.  The output of each is a tabular\n"\
  185. "listing of times and counts.  They may be used with each other, in which\n"\
  186. "case multiple tables are produced.  The only other options that they may\n"\
  187. "be used with are oOnNbpx.\n"\
  188. "-i<interval>: Report accesses for each <interval> period.  <interval> is\n"\
  189. "    given as an integer followed by the letter d, h, or m to indicate\n"\
  190. "    days, hours, or minutes.  Example: -i12h  (INTERVAL)\n"\
  191. "-T<periods>: Report accesses by time of day, for each <periods> fraction\n"\
  192. "    of a day.  (PERIODS)\n"\
  193. "-D: Report accesses by day of week.  (BYDAY)\n",
  194.     Name,Usage,rcFile,rcFile,rcFile,Name,Name,Width
  195.     Err = "0"
  196.     }
  197.     if ("H" in Options) {
  198.     printf \
  199. "%s -r: mail reports of http accesses to users.\n"\
  200. "Usage:\n"\
  201. "%s\n"\
  202. "%s -r examines the http access log and generates reports based on it,\n"\
  203. "which are mailed to the users responsible for particular http-accessible\n"\
  204. "files.  The default is for no report to be sent.  To get a report, users\n"\
  205. "may put the following in a file named %s in their home directories\n"\
  206. "(%s must be a real file; symbolic links are ignored):\n"\
  207. "raw       Raw http access log data, with one line for each access.\n"\
  208. "standard  Summary with one line for each file accessed, inc. access count.\n"\
  209. "wide      This gives the standard report without lines being truncated,\n"\
  210. "      as is usually done to make them fit an 80-column screen.\n"\
  211. "allhits  For each object, the number of accesses and the object name is\n"\
  212. "         printed, followed by a time and hostname for each access.\n"\
  213. "html       Like wide, but HTML-formatted (as in the -y option).\n"\
  214. "Multiple report levels may be specified, spread over one or more lines.\n"\
  215. "All of the requested reports will be sent.\n"\
  216. "Report levels may be prefixed with a tag (separated from the level name by\n"\
  217. "a colon), in which case a report at the given level will only be sent if\n"\
  218. "the tag name is given with the -g option.  If any level has a matching\n"\
  219. "tag, then only the levels with matching tags will be set up.  If no\n"\
  220. "matching tags are found, then all of the levels without tags are set up. \n"\
  221. "If -g is not given, then all of levels without tags are sent.  Example: If\n"\
  222. "-gmonthly is given and \"monthly:standard raw monthly:html\" appears in a\n"\
  223. "user's configuration file, the user will get standard and html reports. \n"\
  224. "For the same configuration file, if -gdaily is given, the user will get a\n"\
  225. "raw report, since no daily tag appears in the configuration file.\n"\
  226. "     The operation of %s -r as a whole is determined by the master\n"\
  227. "configuration file %s.  This file maps patterns to user\n"\
  228. "names and must exist.  The master configuration file should contain lines\n"\
  229. "of the form:\n"\
  230. "Pattern  Address[,...]  [Report-level,...]\n"\
  231. "     Pattern is an egrep(C)-style regular expression to match the pathname\n"\
  232. "of an object that may be accessed.  It is not anchored; begin it with ^ to\n"\
  233. "force a match to start at the beginning of the path.\n"\
  234. "     Address is the email address of the user that reports of accesses that\n"\
  235. "match this pattern should be sent to.  Accesses that match a pattern can\n"\
  236. "be included in the reports sent to multiple users by giving a comma-\n"\
  237. "separated list of email addresses for Address.  If Pattern and Address\n"\
  238. "both contain an embedded %%s, then the %%s in Pattern will be used to\n"\
  239. "match anything up to the next '/', and the %%s in Address will be replaced\n"\
  240. "with whatever was matched by it.  This can be used to send reports to a\n"\
  241. "set of users or aliases whose names will be embedded in URLs.  If %%s\n"\
  242. "substitution is used, the name of the local user whose %s file should\n"\
  243. "be used to determine the levels for a report is taken to be the part that\n"\
  244. "%%s matches, not the Address field after the %%s in it has been substituted\n"\
  245. "for.\n"\
  246. "     Reports is a comma-separated list of report levels.  If given, it\n"\
  247. "overrides the default of %s.  It in turn is overridden if a user requests\n"\
  248. "particular report levels in a %s file.\n"\
  249. "     Reports are sent for all patterns that match, not just the first.\n"\
  250. "     Lines that begin with # are comments and are ignored.\n"\
  251. "     Example mapping file:\n"\
  252. "######\n"\
  253. "# report accesses of objects in users' home pages to the users\n"\
  254. "^/~%%s/    %%s\n"\
  255. "# Report cgi-bin & tests accesses to spcecdt\n"\
  256. "^/(cgi-bin|tests)/      spcecdt\n"\
  257. "# report all transfers other than user home pages to webmaster\n"\
  258. "^/[^~]    webmaster standard\n"\
  259. "######\n"\
  260. "The w option can be used to set the screen width for the 'standard'\n"\
  261. "report level.  The n option can be used but probably shouldn't be.\n"\
  262. "Options specific to -r:\n"\
  263. "-P: Print progress numbers.  (PROGRESS)\n"\
  264. "-M<mail-address>: Mail all reports to <mail-address> (for testing).\n"\
  265. "-s<subject>: Set the subject used in mail.  The default is '%s'.\n"\
  266. "    (SUBJECT)\n"\
  267. "-E<sender-address>: Make report mail come from <sender-address>.  The\n"\
  268. "    default is \"%s\".  (MAILFROM)\n"\
  269. "-Y: Use \"submit\" (the MMDF mail injection command) to submit messages\n"\
  270. "    to the mail queue.  This allows special options to be given to the\n"\
  271. "    mail system; in particular, options are passed that prevent the mail\n"\
  272. "    from being returned if the local mail system cannot deliver it.  If -Y\n"\
  273. "    is not used, mail is injected by invoking \"sendmail\".  (USESUBMIT)\n"\
  274. "-t: Test only; do not actually mail anything.\n"\
  275. "-g<tag>: Set the report level tag.\n"\
  276. "-l<default-report-level>: Set the default report level to something other\n"\
  277. "    than %s.  A comma-separated list of levels can be given.  This is over-\n"\
  278. "    ridden by values given in the systemwide mapfile and in user config\n"\
  279. "    files.  (DEFLEVEL)\n"\
  280. "-L<level-set>: Only generate reports at levels that are named in the comma-\n"\
  281. "    separated list.  Requests for reports at other levels are ignored.\n"\
  282. "    ignored.  This can be useful for e.g. monthly cumulative reports, for\n"\
  283. "    which raw reports might be unwanted or too large to process.  (LEVELS)\n"\
  284. "-U: If multiple users have the same home directory and there is a\n"\
  285. "    configuration file in the directory, all report data generated as\n"\
  286. "    a result of %%s substitution is sent only to the first user with that\n"\
  287. "    home directory listed in the password file.  Example:\n"\
  288. "    Users spcecdt and john both have home directory /u/spcecdt, and there\n"\
  289. "    is a configuration file in that directory.  spcecdt is listed first in\n"\
  290. "    /etc/passwd.  A %%s line in the master configuration file causes a\n"\
  291. "    report on all accesses of objects that begin with /~username/ to be\n"\
  292. "    sent to username.  In this case, accesses of /~john/ will be reported\n"\
  293. "    to spcecdt instead of john.  (CONGLOMUSERS)\n"\
  294. "-c<map-file>: Use <map-file> instead of %s.  (MAPFILE)\n",
  295.     Name,RUsage,Name,userMapfile,userMapfile,Name,ConfigFile,
  296.     userMapfile,DefLevel,userMapfile,Subject,Sender,DefLevel,ConfigFile
  297.     Err = "0"
  298.     }
  299.     if (Err != "")
  300.     exit Err
  301.     if ("x" in Options) {
  302.     Debug = Options["x"]
  303.     if (Debug ~ /^[1-9][0-9]*$/)
  304.         printf "Debugging set to level %d\n",Debug > "/dev/stderr"
  305.     else if (Debug ~ /^[1-9][0-9]*:[1-9][0-9]*$/) {
  306.         split(Debug,elem,":")
  307.         StartDebug = elem[1]
  308.         DebugValue = elem[2]
  309.         Debug = 0
  310.         printf "At record %d, debug level will be set to %d.\n",
  311.         StartDebug,DebugValue > "/dev/stderr"
  312.     }
  313.     else {
  314.         printf "%s: Bad value given with -x: %s\n",
  315.         Name,Debug > "/dev/stderr"
  316.         Err = 1
  317.         exit 1
  318.     }
  319.     }
  320.     CountOnly = "C" in Options
  321.     noBad = !("e" in Options)
  322.     if ("I" in Options)
  323.     ARGV[ARGC++] = "/dev/stdin"
  324.     else if (ARGC < 2)
  325.     if ("f" in Options) {
  326.         numFiles = split(Options["f"],Files,",")
  327.         for (i = 1; i <= numFiles; i++)
  328.         ARGV[i] = Files[i]
  329.         ARGC = numFiles + 1
  330.     }
  331.     else {
  332.         printf "%s: No input files named.\n",Name > "/dev/stderr"
  333.         Err = 1
  334.         exit 1
  335.     }
  336.     if (Debug) {
  337.     print "Files to be processed:" > "/dev/stderr"
  338.     for (i = 1; i <= ARGC; i++)
  339.         print ARGV[i] > "/dev/stderr"
  340.     }
  341.     if ("w" in Options)
  342.     Width = Options["w"]
  343.     Truncate = "d" in Options
  344.     MostAccessed = "a" in Options
  345.     UseSubmit = "Y" in Options
  346.     if ("E" in Options)
  347.     Sender = Options["E"]
  348.     Reprocess = "R" in Options
  349.     CountHosts = "S" in Options
  350.     TrackObjects = !(TrackHosts = ("q" in Options))
  351.     DoHeader = !("b" in Options)
  352.     LogBytes = "B" in Options || CountOnly
  353.     SortBytes = "K" in Options
  354.     AllAccesses = "A" in Options
  355.     TrackLast = !("k" in Options || AllAccesses)
  356.     if (CountOnly)
  357.     TrackObjects = TrackHosts = TrackLast = DoHeader = 0
  358.     if (HTMLout = ("y" in Options))
  359.     Width = 0
  360.     if ("g" in Options)
  361.     Tag = Options["g"]
  362.     if ("r" in Options) {
  363.     if (MailRepSetup(ConfigFile,userMapfile,DefLevel,Tag,"U" in Options)) {
  364.         Err = 1
  365.         exit 1
  366.     }
  367.     }
  368.     else    # Do progress numbers only if they would go to a tty
  369.     Progress = !("p" in Options) && system("test -t 2") == 0
  370.     IGNORECASE = 1
  371.     if ("o" in Options || "O" in Options || "n" in Options || "N" in Options) {
  372.     if ("o" in Options)
  373.         ObjPattern = Options["o"]
  374.     if ("n" in Options)
  375.         HostPattern = Options["n"]
  376.     if ("O" in Options)
  377.         NotObjPattern = Options["O"]
  378.     if ("N" in Options)
  379.         NotHostPattern = Options["N"]
  380.     PatternGiven = 1
  381.     }
  382.     if ("y" in Options || "r" in Options) {
  383.     if ("u" in Options)
  384.         URLpref = Options["u"]
  385.     else {
  386.         CmdReadLine("hostname")
  387.         URLpref = $0
  388.     }
  389.     URLpref = "http://" URLpref
  390.     if (Debug)
  391.         print "URL prefix is " URLpref > "/dev/stderr"
  392.     }
  393.     if ("i" in Options) {
  394.     Interval = Options["i"]
  395.     IntervalUnits = substr(Interval,length(Interval),1)
  396.     Interval += 0
  397.     if (IntervalUnits !~ /^[dhm]$/) {
  398.         printf \
  399.         "Invalid unit '%s' given with -i option.  Must be one of [dhm].\n",
  400.         IntervalUnits > "/dev/stderr"
  401.         Err = 1
  402.         exit(1)
  403.     }
  404.     if (Interval == 0) {
  405.         printf "Invalid value '%s' given with -i option.\n",
  406.         Interval > "/dev/stderr"
  407.         Err = 1
  408.         exit(1)
  409.     }
  410.     if (IntervalUnits == "d")
  411.         Interval *= 86400
  412.     else if (IntervalUnits == "h")
  413.         Interval *= 3600
  414.     else if (IntervalUnits == "m")
  415.         Interval *= 60
  416.     if (Debug)
  417.         printf "Interval is %d seconds.\n",Interval > "/dev/stderr"
  418.     }
  419.     if ("T" in Options) {
  420.     DayFracs = Options["T"]
  421.     BucketSecs = 86400/DayFracs
  422.     }
  423.     DoDayOfWeek = "D" in Options
  424.     if (Interval || DayFracs || DoDayOfWeek) {
  425.     MkMonth2Num()
  426.     MakeTZOffset()
  427.     Counting = 1
  428.     }
  429.     if (Debug)
  430.     print "Done with setup." > "/dev/stderr"
  431. }
  432.  
  433. # Setup actions needed only for allreports mode.
  434. # Global vars initialized here:
  435. # ReportAll    Tells other functions that multiple users are being rept on.
  436. # Test        If no mail should be sent.
  437. # Subject    Subject of mail sent to users (not yet used).
  438. # LevelOrder    Comma-sep list of valid report levels.
  439. # LevelSet[]    Set of valid report levels.
  440. # ChkRaw    If some raw report is requested.
  441. # AllAccesses    If anyone requested an "allhits" report.
  442. # RawSet[]    A set which contains only "raw", to be passed to Log()
  443. # See ReadConf() for a description of Users[] Patterns[] Prefixes[]
  444. # See LevelFixup() for a description of MaybeRaw[] MaybeNotRaw[]
  445. # See ReadUserConfigs() for a description of UserSpec[] UserLevelSet[]
  446. function MailRepSetup(ConfigFile,userMapfile,DefLevel,Tag,ConglomReports,
  447. i,NumRawUser,NumRawConf,NumPat,AllAddrs,LevelLists,LevelCount) {
  448.     #   NumRawUser is the number of users who requested a raw report,
  449.     # passed to LevelFixup so that if it is nonzero, %s report lines can be
  450.     # marked as possibly requiring a raw report.
  451.     #   NumRawConf is the number of patterns that may result in a raw report.
  452.     # It and NumRawUser are used to determine whether any log lines should be
  453.     # checked to determine if they should be attached to any raw reports.
  454.  
  455.     ReportAll = 1        # More than one user is being reported on
  456.     # In allreport mode, print progress numbers only if P is given.
  457.     Progress = ("P" in Options)
  458.     Test = "t" in Options        # Do not mail anything.
  459.     if ("s" in Options)            # Subject of mail sent to users.
  460.     Subject = Options["s"]
  461.     if ("c" in Options)            # Systemwide config file
  462.     ConfigFile = Options["c"]
  463.     LevelOrder = "standard,wide,html,allhits,none,raw"
  464.     MakeSet(LevelSet,LevelOrder,",")    # Valid report levels
  465.     if ("L" in Options) {
  466.     if (CheckLevels(Options["L"],LevelSet,AcceptLevels) < 1)
  467.         return 1
  468.     }
  469.     else
  470.     CopySet(LevelSet,AcceptLevels)
  471.     # Always accept 'none', so that it can be used as the level for %s lines
  472.     # that don't have any accepted level in the default.
  473.     AcceptLevels["none"]
  474.     if ("l" in Options) {        # Change the default level
  475.     DefLevel = Options["l"]
  476.     if (CheckLevels(Options["L"],LevelSet) == -1)
  477.         return 1
  478.     }
  479.     # Read systemwide config file
  480.     if ((NumPat = ReadConf(ConfigFile,DefLevel,Patterns,Users,
  481.     Prefixes,LevelLists,AllAddrs)) < 0)
  482.     return 1
  483.     # Read user config files beforehand to optimize data collection.
  484.     # That way we do not need to keep allhits data if noone requests it,
  485.     # and only need to keep raw report data around for users who request it.
  486.     ReadUserConfigs(UserSpec,UserLevelSet,userMapfile,AcceptLevels,
  487.     !IsEmpty(Prefixes),AllAddrs,Tag,LevelCount,ConglomReports,UserMap)
  488.     NumRawUser = LevelCount["raw"]
  489.     AllAccesses = "allhits" in LevelCount && LevelCount["allhits"] > 0
  490.     if ((NumRawConf = LevelFixup(UserSpec,NumRawUser,MaybeRaw,MaybeNotRaw,
  491.     LevelSet,AcceptLevels,ConfigFile,Levels,NumPat,LevelLists)) == -1)
  492.     return 1
  493.     if (NumRawConf > 0 || NumRawUser > 0) {
  494.     ChkRaw = 1
  495.     if (Debug)
  496.         print "Raw logging is on." > "/dev/stderr"
  497.     }
  498.     RawSet["raw"]
  499.     if (Debug) {
  500.     for (i = 0; i < NumPat; i++) {
  501.         printf "Pattern %d: <%s>  Levels: <%s>",
  502.         i,Patterns[i],LevelLists[i] > "/dev/stderr"
  503.         if (i in Users)
  504.         printf "  User: <%s>",Users[i] > "/dev/stderr"
  505.         if (i in Prefixes)
  506.         printf "  Prefix: <%s>",Prefixes[i] > "/dev/stderr"
  507.         if (i in MaybeRaw)
  508.         printf "  MaybeRaw" > "/dev/stderr"
  509.         if (i in MaybeNotRaw)
  510.         printf "  MaybeNotRaw" > "/dev/stderr"
  511.         print "" > "/dev/stderr"
  512.     }
  513.     }
  514. }
  515.  
  516. function MkMonth2Num(  Month) {
  517.     split("Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec",Months,",")
  518.     for (Month in Months)
  519.     Month2Num[Months[Month]] = sprintf("%02d",Month)
  520. }
  521.  
  522. # Sets global TZOffset to the number of seconds that need to be substracted
  523. # from the local date (without time of day) to give an epoch time.
  524. # TZOffset can also be added to systime() before doing %86400 to get the
  525. # current day number in the local timezone.
  526. # Note that TZOffset is only correct if the given date is in the same DST
  527. # phase as the current date.
  528. # 95/03/26 Calculate TZOffset more accurately.
  529. function MakeTZOffset(  t) {
  530.     t = systime()
  531.     TZOffset = strftime("%H",t)*3600+strftime("%M",t)*60+strftime("%S",t) - \
  532.     t%86400
  533.     if (strftime("%j",0) != "001")
  534.     TZOffset -= 24*3600
  535. }
  536.  
  537. # Convert a numeric timezone to a number of seconds.
  538. # Example: converts -0830 to -30600
  539. function TZ2sec(NTimezone) {
  540.     if (NTimezone < 0) {
  541.     NTimezone = substr(NTimezone,2)
  542.     Mult = -1
  543.     }
  544.     else
  545.     Mult = 1
  546.     return (substr(NTimezone,1,2)*3600+substr(NTimezone,3,2)*60)*Mult
  547. }
  548.  
  549. # Converts comma-separated level list Levels to lower case,
  550. # splits it into LevelSet, and checks it against GoodLevels.
  551. # Returns -1 on success, else number of levels.
  552. # Filename is the file that Levels was read from, for error messages.
  553. # If not given, it is not included.
  554. function CheckLevels(Levels,GoodLevels,LevelSet,Filename,
  555. LevElem,Level,NumLevels,i,RealLev) {
  556.     RealLev = 0
  557.     NumLevels = split(Levels,LevElem,",")
  558.     split("",LevelSet)        # clear LevelSet
  559.     for (i = 1; i <= NumLevels; i++) {
  560.     Level = LevElem[i]
  561.     if (!(Level in GoodLevels)) {
  562.         if (Filename == "")
  563.         printf "Bad level '%s'.  Exiting.\n",Level > "/dev/stderr"
  564.         else
  565.         printf \
  566.         "Error in config file '%s': bad level '%s'.  Exiting.\n",
  567.         Filename,Level > "/dev/stderr"
  568.         return -1
  569.     }
  570.     else if (!(Level in LevelSet)) {
  571.         LevelSet[Level]
  572.         RealLev++
  573.     }
  574.     }
  575.     return RealLev
  576. }
  577.  
  578. # Input vars:
  579. # ConfigFile is the master configuration file to read.
  580. # The configuration file has this format:
  581. # pattern       recipient[,...]       [report-level,...]
  582. # DefLevel is a list of levels.  Lines that have no report-levels specified
  583. # will default to these levels.
  584. #
  585. # Output vars: 
  586. # AllAddrs[] is made the set of all addresses that reports may
  587. # be sent to (all recipient names found), for use by ReadUserConfigs() in
  588. # finding the configuration files for those recipients.
  589. # All of the rest of these arrays are indexed 0..(numPatterns-1):
  590. # Patterns[] contains the pattern that the data for each index applies to.
  591. # In the case of %s substitutions, Patterns[] will contain the given pattern
  592. # with the %s replaced by "[^/]*", since that is what the %s stands in for.
  593. # Users[] contains the email address of the user who should gets reports of
  594. #     matches to this pattern.
  595. # Prefixes[] contains the part of a path that comes before the user name for
  596. #     patterns that apply to more than one user (%s lines).
  597. # LevelLists[] contains the default report level for matches to this pattern.
  598. #
  599. # Return value: <0 for error.  If no error, returns the number of entries
  600. #     put in the arrays.
  601. function ReadConf(ConfigFile,DefLevel,Patterns,Users,Prefixes,
  602. LevelLists,AllAddrs,  ret,Pattern,User,LineNum,UserElem,i,LevList) {
  603.     NumPat = 0
  604.     while ((ret = (getline < ConfigFile)) == 1) {
  605.     LineNum++
  606.     if ($0 ~ /^#/)        # Comment
  607.         continue
  608.     if (NF < 2) {
  609.         printf \
  610.         "Error on line %d of config file %s: Not enough fields:\n%s\n",
  611.         LineNum,ConfigFile,$0
  612.         return -1
  613.     }
  614.     if (Debug)
  615.         printf "Config line: <%s>\n",$0 > "/dev/stderr"
  616.     Pattern = $1
  617.     User = $2
  618.     # Generate a separate entry for each user.
  619.     # This is faster than splitting later for each match because in
  620.     # most cases there will only be one.
  621.     split(User,UserElem,",")
  622.     for (i in UserElem) {
  623.         User = UserElem[i]
  624.         if (Debug)
  625.         printf "  User: %s\n",User > "/dev/stderr"
  626.         if (NF > 2) {
  627.         $1 = $2 = ""
  628.         LevList = $0
  629.         gsub("[ \t]+","",LevList)
  630.         }
  631.         else
  632.         LevList = DefLevel
  633.  
  634.         # Convert %s line
  635.         if (Pattern ~ /%s/ && User ~ /%s/) {
  636.         Prefixes[NumPat] = Pattern
  637.         sub(/%s.*/,"",Prefixes[NumPat])
  638.         sub("%s","[^/]*",Pattern)
  639.         # Make User consisting solely of %s a special case
  640.         # (by not recording in it Users[]),
  641.         # for a little speedup when processing logfile.
  642.         if (User != "%s")
  643.             Users[NumPat] = User
  644.         }
  645.         else
  646.         Users[NumPat] = User
  647.         Patterns[NumPat] = Pattern
  648.         LevelLists[NumPat] = LevList
  649.         AllAddrs[User]
  650.  
  651.         NumPat++
  652.     }
  653.     }
  654.     if (ret) {
  655.     printf "Error reading config file '%s'.  Exiting.\n",ConfigFile > \
  656.     "/dev/stderr"
  657.     return -1
  658.     }
  659.     return NumPat
  660. }
  661.  
  662. # Input variables:
  663. # UserSpec[] contains the list of report levels given by each user who 
  664. #     requests specific levels.
  665. # RawSpec is true if any users requested a raw report.
  666. # LevelSet[] is the set of valid level names.
  667. # AcceptLevels[] is the set of levels that reports may be requested at.
  668. # ConfigFile is the name of the config file that was read.
  669. # NumPat is the number of elements in Patterns[].
  670. # LevelLists[] contains the default report level for matches to the pattern
  671. # with the same index in Patterns[].
  672.  
  673. # Global variables:
  674. # Users[] contains the email address of the user who should gets reports of
  675. #     matches to this pattern.
  676. # Prefixes[] contains the part of a path that comes before the user name for
  677. #     patterns that apply to more than one user.
  678. # Patterns[] contains the pattern that the data for each index applies to.
  679.  
  680. # Output variables:
  681. # MaybeRaw[] contains the indexes of any patterns that may result in a raw
  682. #     report line.
  683. # MaybeNotRaw[] contains the indexes of any patterns that may result in a
  684. #     non-raw report line.
  685. # An index (i,Level) is made in Levels[] for each report level Level requested
  686. # for pattern i.
  687.  
  688. # Return value: the number of patterns that result in a raw report.
  689.  
  690. function LevelFixup(UserSpec,RawSpec,MaybeRaw,MaybeNotRaw,LevelSet,
  691. AcceptLevels,ConfigFile,Levels,NumPat,LevelLists,
  692. Level,LevList,PatLevels,NumRaw) {
  693.     for (i = 0; i < NumPat; i++) {
  694.     if (i in Users && Users[i] in UserSpec) {
  695.         LevelLists[i] = LevList = UserSpec[Users[i]]
  696.         if (Debug)
  697.         printf "Changed report level for pattern %d to %s\n",
  698.         i,LevList > "/dev/stderr"
  699.     }
  700.     else {
  701.         LevList = LevelLists[i]
  702.         if (Debug)
  703.         printf "Kept report level for pattern %d at default of %s\n",
  704.         i,LevList > "/dev/stderr"
  705.     }
  706.     if ((CheckLevels(LevList,LevelSet,PatLevels,ConfigFile)) == -1)
  707.         return -1
  708.     # If this level uses %s substitution, and no levels in it are accepted,
  709.     # then add "none" (which is always accepted) to its list of levels,
  710.     # because some user may have specified a level that is accepted.
  711.     if (i in Prefixes && Intersection(PatLevels,AcceptLevels) == 0)
  712.         PatLevels["none"]
  713.     for (Level in PatLevels) {
  714.         if (!(Level in AcceptLevels))
  715.         continue
  716.         # If this level is raw, or uses %s sustitution and any user
  717.         # requested a raw report, then it may result in a raw report
  718.         # line.
  719.         if (Level == "raw" || RawSpec > 0 && (i in Prefixes)) {
  720.         MaybeRaw[i]
  721.         if (Level == "raw")
  722.             NumRaw++
  723.         }
  724.         # If this level isn't raw, or uses %s substitution, it may result
  725.         # in a non-raw report line.
  726.         if (Level != "raw" || (i in Prefixes))
  727.         MaybeNotRaw[i]
  728.         Levels[i,Level]
  729.     }
  730.     }
  731.     return NumRaw
  732. }
  733.  
  734. # Find all user config files.
  735. # Any user with a config file is made an index of UserSpec, with the value
  736. # being the list of report levels requested.
  737. # Also, for each level a user requests a report at, (User,Level) is made
  738. # an index of UserLevelSet[].
  739. # AllAddrs[] is the set of all addresses that reports may be sent to.
  740. # If no %s substitutions were called for in the master configuration file,
  741. # user configuration files are only searched for in the home directories of
  742. # the users given in AllAddrs[].
  743. # Tag is a tag that a level may be prefixed with (separated from it by a
  744. # colon).  If Tag is given and any levels prefixed by it are found in a user's
  745. # config file, only those levels are set up for the user.  Otherwise, even if
  746. # Tag is given, all levels that do not have a tag are set up.
  747. # LevelCount[] is returned with the number of users who requested a report at
  748. # each level.  E.g., LevelCount["raw"] is the number of users who requested a
  749. # raw report.
  750. # If ConglomReports is true, than the second & later instances of a particular
  751. # home directory found cause an entry added to UserMap, mapping the user whose
  752. # home directory it is to the first user who had that home directory.
  753. # Information for these users is still recorded in the rest of the arrays,
  754. # since ConglomReports applies only to implicit user names generated from %s
  755. # substitution.
  756. function ReadUserConfigs(UserSpec,UserLevelSet,userMapfile,GoodLevels,PctSUsed,
  757. AllAddrs,Tag,LevelCount,ConglomReports,UserMap,
  758. Cmd,User,Home,ConfFile,i,oIgnCase,Level,UserLevels,Elem,GotTag,Count) {
  759.     oIgnCase = IGNORECASE
  760.     IGNORECASE = 1
  761.     # Use sh to get config file list because it can check for symlinks &
  762.     # non-regular files... a weak attempt at security
  763.     if (PctSUsed)
  764.     # If any %s lines were given, get config files for all users
  765.     Cmd = \
  766. "IFS=:\n"\
  767. "while read user p u g n homedir s\n"\
  768. "do\n"\
  769. "    UserConfigFile=$homedir/" userMapfile "\n"\
  770. "    if [ -f $UserConfigFile -a ! -L $UserConfigFile ]; then\n"\
  771. "    echo $user $homedir\n"\
  772. "    fi\n"\
  773. "done < /etc/passwd"
  774.     else {
  775.     # If no %s lines were given, get config files only for users mentioned
  776.     # in global config file; much faster on a system with many users.
  777.     FS = ":"
  778.     while ((getline < "/etc/passwd") == 1)
  779.         if ($1 in AllAddrs)
  780.         Cmd = Cmd " " $1 " " $6
  781.     close("/etc/passwd")
  782.     FS = "[ \t]+"
  783.     if (Debug)
  784.         print "User/homedir list: " Cmd > "/dev/stderr"
  785.     Cmd = \
  786. "set -- " Cmd "\n"\
  787. "while [ $# -ge 2 ]; do\n"\
  788. "    UserConfigFile=$2/" userMapfile "\n"\
  789. "    [ -f $UserConfigFile -a ! -L $UserConfigFile ] && echo $1 $2\n"\
  790. "    shift;shift\n"\
  791. "done"
  792.     }
  793.  
  794.     # Read files at the same time that the filelist is being generated so that
  795.     # it will be harder to take advantage of the window
  796.     while ((Cmd | getline) == 1) {
  797.     User = $1
  798.     Home = $2
  799.     if (ConglomReports)
  800.         if (Home in Home2User) {
  801.         UserMap[User] = Home2User[Home]
  802.         if (Debug)
  803.             printf "Implicit %%s reports for %s go to %s\n",User,
  804.             Home2User[Home] > "/dev/stderr"
  805.         }
  806.         else
  807.         Home2User[Home] = User
  808.     if (Debug)
  809.         printf "%s has a config file.\n",User > "/dev/stderr"
  810.     ConfFile = Home "/.httpreport"
  811.     split("",UserLevels)    # clear out the array.
  812.     GotTag = Count = 0
  813.     while ((ret = (getline < ConfFile)) == 1) {
  814.         for (i = 1; i <= NF; i++) {
  815.         Level = tolower($i)
  816.         if (split(Level,Elem,":") > 1) {
  817.             if (Debug)
  818.             printf "Tag is <%s>, level is <%s>\n",Elem[1],
  819.             Elem[2] > "/dev/stderr"
  820.             if (Tag == "" || Elem[1] != Tag) {
  821.             if (Debug)
  822.                 printf "Skipping non-matching tag <%s>\n",
  823.                 Elem[1] > "/dev/stderr"
  824.             continue
  825.             }
  826.             if (!GotTag) {
  827.             # User gave a tag that matches the report being run,
  828.             # so clear out anything else that has accumulated.
  829.             if (Debug)
  830.                 printf "Removing %d untagged level(s)\n",
  831.                 Count > "/dev/stderr"
  832.             split("",UserLevels)
  833.             GotTag = 1
  834.             Count = 0
  835.             }
  836.             Level = Elem[2]
  837.         }
  838.         # If user previously specified a tag that matches the report
  839.         # being run, and this level has no tag, skip it.
  840.         else if (GotTag) {
  841.             if (Debug)
  842.             printf "Skipping untagged level <%s>\n",
  843.             Level > "/dev/stderr"
  844.             continue
  845.         }
  846.         # Just ignore anything not in GoodLevels.  Nothing obvious to
  847.         # do about bad levels except mail a warning to users, which
  848.         # is liable to just result in bloated mailboxes.  Also, for
  849.         # now the -L option causes the list of good levels passed
  850.         # to this function to be reduced.
  851.         if (Level in GoodLevels) {
  852.             if (Debug)
  853.             printf "Saving level <%s>\n", Level > "/dev/stderr"
  854.             UserLevels[++Count] = Level
  855.         }
  856.         }
  857.     }
  858.     for (i = 1; i in UserLevels; i++) {
  859.         Level = UserLevels[i]
  860.         UserLevelSet[User,Level]
  861.         if (User in UserSpec)
  862.         UserSpec[User] = UserSpec[User] "," Level
  863.         else
  864.         UserSpec[User] = Level
  865.         LevelCount[Level]++
  866.         if (Debug)
  867.         printf "%s gets a report for level '%s'.\n",
  868.         User,Level > "/dev/stderr"
  869.     }
  870.     if (Debug && ret)
  871.         printf "Could not read config file '%s'.\n",
  872.         ConfFile > "/dev/stderr"
  873.     close(ConfFile)
  874.     }
  875.     close(Cmd)
  876.     IGNORECASE = oIgnCase
  877. }
  878.  
  879. # Returns 1 if Set is empty, 0 if not.
  880. function IsEmpty(Set,  i) {
  881.     for (i in Set)
  882.     return 0
  883.     return 1
  884. }
  885.  
  886. # MakeSet: make a set from a list.
  887. # An index with the name of each element of the list
  888. # is created in the given array.
  889. # Input variables: 
  890. # Elements is a string containing the list of elements.
  891. # Sep is the character that separates the elements of the list.
  892. # Output variables:
  893. # Set is the array.
  894. # Return value: the number of elements added to the set.
  895. function MakeSet(Set,Elements,Sep,  i,Num,Names) {
  896.     Num = split(Elements,Names,Sep)
  897.     for (i = 1; i <= Num; i++)
  898.     Set[Names[i]]
  899.     return Num
  900. }
  901.  
  902. ##### End of setup functions.
  903. ##### Start of logging functions.
  904.  
  905. # YMD2day(year,month,day-of-month) returns the number of days that passed from 
  906. # 1970 Jan 1 to the given date.
  907. # All parameters should be given in numeric form.
  908. # If year < 70, it is assumed to be part of the 2000 century
  909. # If year in (70..99), 1900.
  910. # Globals: sets and uses MDays[]
  911. function YMD2day(Year,Month,Day,   LeapDays) {
  912.     if ((Year+0) < 70)
  913.     Year += 100
  914.     else if ((Year+0) >= 100)
  915.     Year -= 1900
  916.     # Year is now the number of years since 1900.
  917.     LeapDays = int((Year - 68) / 4)
  918.     if (Month <= 2 && Year % 4 == 0)
  919.     LeapDays -= 1
  920.     if (!MDays[2])
  921.     split("0 31 59 90 120 151 181 212 243 273 304 334 365",MDays)
  922.     return (Year - 70) * 365 + MDays[Month + 0] + Day - 1 + LeapDays
  923. }
  924.  
  925. # Strip args from cgi accesses
  926. # Globals: none.
  927. function CGIstrip(Object) {
  928.     if (Object ~ "^/cgi-bin/") {
  929.     Object = substr(Object,10)
  930.     sub("[/?].*","",Object)
  931.     Object = "/cgi-bin/" Object
  932.     }
  933.     return Object
  934. }
  935.  
  936. # Input variables:
  937. # Object is the object to try matches against.
  938. # MaybeRept[] contains the indexes of each pattern that may result in a
  939. #     report line (for optimization).
  940. # LevelsToLog[] is the set of levels to log.
  941. # Data is the data to be stored in Reports[] for matches.
  942. #
  943. # Output variables:
  944. # Report data are put in Reports[email-addr,Level,1..n]
  945. # The extra index of 1..n is used rather than just appending strings because
  946. # string concatenation becomes very expensive in awk.
  947. # Global UserLevelSet[email-addr,Level] contains the current highest highest
  948. #     index used in Reports, for tracking the index.  The indexes of ReptLevels
  949. #     are also used to allow iteration over the indexes of Reports[].
  950. # MailNames[email-addr] contains the set of all email-addrs that reports will
  951. #     be sent to, for iterating over the contents of ReptLevels[].
  952. #
  953. # Globals used:
  954. # Patterns[] contains the pattern that the data for each index applies to.
  955. # Users[] contains the email address of the user who should gets reports of
  956. #     matches to this pattern.
  957. # Prefixes[] contains the part of a path that comes before the user name for
  958. #     patterns that apply to more than one user.
  959. # UserSpec[] contains the names of users who have specified what report levels
  960. #     they want.
  961. # UserLevelSet[] contains a User,Level index for each report level that a user
  962. #     requested.
  963. # Levels[] contains the report levels for each pattern.
  964. # UserMap[addr] may remap certain implicit email addresses (produced by %s
  965. #     substitution) to others.
  966.  
  967. # For each index in MaybeRept[], the pattern with that index is checked for a
  968. # match against Object.  If matched, the login name and user name are derived
  969. # from it using Users[] and Prefixes[].
  970. # Then, for each level that is being logged at this stage, if the user
  971. # requested a report at that level, the data is added to the report for that
  972. # level for the user.
  973. function Log(Object,MaybeRept,Reports,LevelsToLog,Data,
  974. Level,User,Login,i,LEnd) {
  975.     for (i in MaybeRept) {
  976.     if (Object ~ Patterns[i]) {
  977.         # Get login name and email address.
  978.         if (i in Prefixes) {    # If doing a %s substitution...
  979.         # Get rid of the part that matches the prefix
  980.         match(Object,Prefixes[i])
  981.         Login = substr(Object,RLENGTH+1)
  982.         # Get rid of everything from the next / onward
  983.         if (LEnd = index(Login,"/"))
  984.             Login = substr(Login,1,LEnd-1)
  985.         if (Login in UserMap) {
  986.             if (Debug > 1)
  987.             printf "Redirecting report for %s to %s\n",
  988.             Login,UserMap[Login] > "/dev/stderr"
  989.             Login = UserMap[Login]
  990.         }
  991.         # Login is now the user-name part of the object.
  992.         if (i in Users) {
  993.             # Subsitute what's left into the email address format
  994.             # string contained in Users[]
  995.             User = Users[i]
  996.             sub("%s",Login,User)
  997.         }
  998.         else
  999.             # The special case of no entry in Users[] indicates that 
  1000.             # the email address is just the user-name.
  1001.             User = Login
  1002.         }
  1003.         else     # If not doing a %s substitution...
  1004.         Login = User = Users[i]
  1005.         # Login is a local user name who might have an entry in UserSpec[].
  1006.         # User is the email address that a report should be sent to.
  1007.         for (Level in LevelsToLog) {
  1008.         # If user requested specific levels & did not include this one,
  1009.         # or user didn't request specific levels and this level is not
  1010.         # in the default set of levels for this pattern, skip it.
  1011.         if (Login in UserSpec) {
  1012.             if (!((Login,Level) in UserLevelSet))
  1013.             continue
  1014.         }
  1015.         else if (!((i,Level) in Levels))
  1016.             continue
  1017.         Reports[User,Level,++UserLevelSet[User,Level]] = Data
  1018.         MailNames[User]
  1019.         if (Debug > 1)
  1020.             printf "Report on '%s' at level '%s' goes to '%s'.\n",
  1021.             Object,Level,User > "/dev/stderr"
  1022.         }
  1023.     }
  1024.     }
  1025. }
  1026.  
  1027. # Return date in the form: MMM dd hh:mm
  1028. # Old format:
  1029. #  3  4     5     6
  1030. #      1  2  3
  1031. # Feb 19 17:53:27 1995
  1032. # New format:
  1033. #       4              5
  1034. #   2  3   4    5 6  7     8
  1035. # [15/Mar/1995:18:00:30 -0800] 
  1036. # [15/Mar/1995:18:00:30] 
  1037. # Globals:
  1038. # NewFormat
  1039. function Date(  E) {
  1040.     if (Reprocess)
  1041.     return sprintf("%s %2d %5s",$2,$3,$4)
  1042.     else if (NewFormat) {
  1043.     split($4,E,"[][/: ]")
  1044.     return sprintf("%s %2d %2s:%2s",E[3],E[2],E[5],E[6])
  1045.     }
  1046.     else
  1047.     return sprintf("%s %2d %5s",$3,$4,substr($5,1,5))
  1048. }
  1049.  
  1050. # Returns the time the current entry was made, as though it occured in the
  1051. # current timezone.  This is done instead of adding the timezone offset
  1052. # (which would make the returned value be a 'real' UNIX time) because we
  1053. # want to dump entries into buckets/intervals based on the time in the local
  1054. # timezone.
  1055. function EntryTime(  E,Time) {
  1056.     if (NewFormat) {
  1057.     split($4,E,"[][/: ]")
  1058.     Time = YMD2day(E[4],Month2Num[E[3]],E[2])*86400 + E[5]*3600 + E[6]*60
  1059.     }
  1060.     else {
  1061.     split($5,E,":")
  1062.     Time = YMD2day($6,Month2Num[$3],$4)*86400 + E[1]*3600 + E[2]*60
  1063.     }
  1064.     if (Debug > 5)
  1065.     printf "Entry time is %s for %s\n",strftime("%c",Time-TZOffset),
  1066.     Uncontrol($0) > "/dev/stderr"
  1067.     if (NR == 1)
  1068.     FirstTime = Time
  1069.     LastTime = Time
  1070.     return Time
  1071. }
  1072.  
  1073. # Do the work common to processing new data and reprocessing processed data.
  1074. # Uses globals:
  1075. # Object    object logged
  1076. # Truncate    Whether to truncate objects to 1st path component.
  1077. # TrackObjects    Track object accesses.
  1078. # TrackHosts    Track hosts.
  1079. # PatternGiven, ObjPattern, NotObjPattern, HostPatter, NotHostPattern
  1080. # Progress    Print progress numbers.
  1081. # Sets globals:
  1082. # LastRemote[] to what the last access of the object or host was
  1083. # Index to the index that the info about this access should be logged under
  1084. # Return value: 0 if there should be no further processing (used instead of
  1085. # doing a 'next' here to avoid gawk memory leak).
  1086. function Common(  SStart) {
  1087.     if (PatternGiven && (ObjPattern != "" && Object !~ ObjPattern ||
  1088.     NotObjPattern != "" && Object ~ NotObjPattern ||
  1089.     NotHostPattern != "" && Host ~ NotHostPattern ||
  1090.     HostPattern != "" && Host !~ HostPattern)) {
  1091.     if (Debug > 2)
  1092.         printf \
  1093.         "Skipping record; object or host does not match pattern:\n"\
  1094.         "%s\n",Uncontrol($0) > "/dev/stderr"
  1095.     NotMatch++
  1096.     return 0
  1097.     }
  1098.     if (Progress && ++pCount == 1000) {
  1099.     printf "\r%d",NR > "/dev/stderr"
  1100.     pCount = 0
  1101.     }
  1102.     if (Counting)
  1103.     return 1
  1104.     if (Truncate) {
  1105.     # Require object to have a / other than the initial one
  1106.     if (SStart = match(Object,"./"))
  1107.         # Truncate before the second /
  1108.         Object = substr(Object,1,SStart)
  1109.     else if (substr(Object,1,2) == "/~")
  1110.         Object = "/~"
  1111.     else
  1112.         Object = "/"
  1113.     }
  1114.     if (TrackObjects) {
  1115.     Index = Object
  1116.     if (TrackLast)
  1117.         LastRemote[Index] = Host
  1118.     }
  1119.     else if (TrackHosts) {
  1120.     Index = Host
  1121.     if (TrackLast)
  1122.         LastRemote[Index] = Object
  1123.     }
  1124.     else if (CountOnly)
  1125.     Index = "TOTAL"
  1126.     return 1
  1127. }
  1128.  
  1129. NR == StartDebug {
  1130.     Debug = DebugValue
  1131. }
  1132.  
  1133. Debug > 9 {
  1134.     printf "Record %d: %s\n",NR,Uncontrol($0) > "/dev/stderr"
  1135. }
  1136.  
  1137. # Example lines from "old" httpd log:
  1138. #      1              2   3   4    5       6    7     8            9
  1139. # Requesting-host    Date            Op   URL        Version
  1140. # deeptht.armory.com [Sat Feb 19 17:53:27 1994] GET  /~spcecdt/arm.html HTTP/1.0
  1141. # sgil301.cern.ch    [Mon Jul 18 07:35:42 1994] POST /cgi-bin/purity-test/NumQuest=100/Name=Sex100 HTTP/1.0
  1142. # netcom5.netcom.com [Sun May 22 00:17:16 1994] get  /u/css1217/index.html
  1143. # pentlan.stir.ac.uk [Wed May 25 01:17:29 1994] HEAD /~zap/nc/nc.html HTTP/1.0
  1144.  
  1145. # New transfer log format:
  1146. # (see http://hoohoo.ncsa.uiuc.edu/docs/setup/httpd/TransferLog.html)
  1147. # host rfc931 authuser [DD/Mon/YYYY:hh:mm:ss] "method resource protocol" status-code bytes-sent referer agent
  1148. # rfc391, authuser, status-code, and bytes-sent are replaced with '-' if they
  1149. # are not available.
  1150. # resource is liable to consist of multiple words, and even a newline
  1151. # occasionally, which makes part of it appear to be a new record.  This is
  1152. # dealt with by ignoring methods other than GET, POST, and HEAD (which also
  1153. # skips uninteresting methods like OPTIONS).
  1154. # Both resource and referrer are liable to contain embedded double quotes.
  1155. # Spinner logs are liable to be completely missing the host field, thus
  1156. # starting with the space that separates the first field from the second.
  1157. # This is OK because FS is set to a pattern that causes the empty first field
  1158. # to still count as a field.
  1159.  
  1160. # Example:
  1161. #   1      2 3             4           5      6  7    8+      9+  10+  11+ 12+
  1162. # x.mv.com - - [15/Mar/1995:18:00:30 -0800] "GET / HTTP/1.0" 200 34110 ref agn
  1163. #    1       2   3           4              5  6    7+      8+   9+  11+ 12+
  1164. # x.mv.com blort - [15/Mar/1995:18:00:30] "GET / HTTP/1.0" 200 34110 ref agn
  1165.  
  1166. # Header of processed output:
  1167. # http accesses, Apr 13 01:18 to Apr 13 18:15.
  1168.  
  1169. # Determine datafile type.  This must come before the Reprocess block, since it
  1170. # does reprocessing autorecognition.
  1171. NR == 1 {
  1172.     # Use ~ for field 1 so it will match regardless of case; format changed
  1173.     if ($1 ~ "http" && $2 == "accesses,") {
  1174.     if (Debug)
  1175.         print "Datafile is old output." > "/dev/stderr"
  1176.     Reprocess = 1
  1177.     if (Counting) {
  1178.         print "Cannot use old output file for interval counting." \
  1179.         > "/dev/stderr"
  1180.         exit(1)
  1181.     }
  1182.     MinFields = 6
  1183.     NotRecs++
  1184.     }
  1185.     else {
  1186.     if (NewFormat = ($4 ~ /^\[/)) {
  1187.         if (Debug)
  1188.         print "Datafile is in the new format." > "/dev/stderr"
  1189.         if ($4 == "[")
  1190.         print "Warning: datafile has no dates?!" > "/dev/stderr"
  1191.         if ($4 ~ /\]$/) {
  1192.         if (Debug)
  1193.             print "Datafile has no timezone field." > "/dev/stderr"
  1194.         Op_i = 5
  1195.         URL_i = 6
  1196.         Bytes_i = 9
  1197.         if (noBad)
  1198.             SC_i_start = 8
  1199.         MinFields = 9
  1200.         }
  1201.         else {
  1202.         if (Debug)
  1203.             print "Datafile has a timezone field." > "/dev/stderr"
  1204.         Op_i = 6
  1205.         URL_i = 7
  1206.         Bytes_i = 10
  1207.         if (noBad)
  1208.             SC_i_start = 9
  1209.         MinFields = 10
  1210.         }
  1211.         MakeSet(Methods,"\"GET,\"POST,\"HEAD",",")
  1212.         # Codes actually used by spinner:
  1213.         # 200 Success
  1214.         # 302 Redirect (spinner sends this for both explicitly set
  1215.         #     redirects and to redirect /~foo to /~foo/index.html)
  1216.         # 304 Not Modified
  1217.         # 404 Not Found
  1218.         # 403 Forbidden
  1219.         # 500 Internal Error
  1220.         MakeSet(StatusCodes,
  1221.     "200,201,202,203,204,400,401,402,403,404,500,501,502,503,301,302,303,304",
  1222.         ",")
  1223.     }
  1224.     else {
  1225.         if (Debug)
  1226.         print "Datafile is in the old format." > "/dev/stderr"
  1227.         Op_i = 7
  1228.         URL_i = 8
  1229.         MakeSet(Methods,"GET,POST,HEAD",",")
  1230.         LogBytes = 0
  1231.         MinFields = 8
  1232.     }
  1233.     StartDate = Date()
  1234.     }
  1235.     Host_i = 1
  1236.     if (Debug)
  1237.     printf "Method index: %d\n",Op_i > "/dev/stderr"
  1238. }
  1239.  
  1240. Reprocess && $2 != "Total" {
  1241.     sub("^ +","")
  1242.  
  1243.     # Check for header, total line, etc.
  1244.     # Header line: http accesses, Sep 23 00:15 to Sep 23 08:06.
  1245.     # Log lines:
  1246.     # count mon  d time   host                bytes?   object
  1247.     #     1  2   3  4      5                     6?        6/7
  1248.     #    11 Feb  1 15:47  becnet51.becnet.com  99999   /robots.txt
  1249.     if (($1 + 0) == 0) {
  1250.     # Since dates are not in order in processed output, get them from
  1251.     # the old headers.
  1252.     if ($6 == "to") {
  1253.         if (Debug)
  1254.         printf "Got log interval line.\n" > "/dev/stderr"
  1255.         if (StartDate == "")
  1256.         StartDate = sprintf("%s %2d %5s",$3,$4,$5)
  1257.         EndDate = sprintf("%s %2d %5s",$7,$8,substr($9,1,5))
  1258.         NotRecs++
  1259.         next
  1260.     }
  1261.     else if ($1 == "Num" && $NF == "Object") {
  1262.         if (NF == 6 || NF == 7)
  1263.         GotLast = 1
  1264.         else if (NF == 2 || NF == 3)
  1265.         TrackLast = 0
  1266.         else {
  1267.         printf \
  1268.     "%s: Unrecognized output-file format - %d fields in header.  Exiting.\n",
  1269.         Name,NF > "/dev/stderr"
  1270.         exit 1
  1271.         }
  1272.         if (!(NF == 3 || NF == 7))
  1273.         LogBytes = 0
  1274.         NotRecs++
  1275.         next
  1276.     }
  1277.     if (Debug)
  1278.         printf "Skipping record: wrong number of fields (%d);"\
  1279.         " presumably header/trailer line:\n%s\n",
  1280.         NF,Uncontrol($0) > "/dev/stderr"
  1281.     WrongNumFields++
  1282.     next
  1283.     }
  1284.     # Set these for Common()
  1285.     Object = $NF
  1286.     if (GotLast)
  1287.     Host = $5
  1288.  
  1289.     if (Common()) {
  1290.     if (LogBytes && $(NF-1) ~ /^[0-9]+$/)
  1291.         Bytes[Index] += $(NF - 1)
  1292.     Count[Index] += $1
  1293.     if (TrackLast)
  1294.         LastXferDate[Index] = Date()
  1295.     }
  1296.     next
  1297. }
  1298.  
  1299. # For each access line, the count of the object accessed in incremented, and
  1300. # the line is added to the raw logs of any users who have requested a raw log.
  1301. NF >= MinFields && toupper($Op_i) in Methods {
  1302.     # Skip failed requests.
  1303.     # Search for response code starting at SC_i_start.  If object contains
  1304.     # embedded spaces, response code will be shifted to a higher field.
  1305.     if (SC_i_start) {
  1306.     # Check up through field NF - 1; some logs do not contain referrer or agent
  1307.     for (SC_i = SC_i_start; SC_i < NF; SC_i++) {
  1308.         if (Debug > 6 && SC_i > SC_i_start)
  1309.         printf "Record %d: searching field %d for status...\n",
  1310.         NR,SC_i > "/dev/stderr"
  1311.         if ($SC_i ~ /^[0-9][0-9][0-9]$/)
  1312.         break
  1313.     }
  1314.     if (Debug) {
  1315.         if (Debug > 1 && SC_i > SC_i_start)
  1316.         printf "Record %d: found status (%s) in field %d, not %d\n",
  1317.         NR,$SC_i,SC_i,SC_i_start > "/dev/stderr"
  1318.         if (!($SC_i in StatusCodes))
  1319.         printf "Record %d: Unknown status code: %s\n%s\n",
  1320.         NR,$SC_i,$0 > "/dev/stderr"
  1321.     }
  1322.     if (SC_i == NF || $SC_i ~ /^[45]/) {
  1323.         if (SC_i == NF) {
  1324.         if (Debug > 2)
  1325.             printf "Skipping record %d: couldn't find status:\n%s\n",
  1326.             NR,Uncontrol($0) > "/dev/stderr"
  1327.         NoStatus++
  1328.         }
  1329.         else
  1330.         HTTPFailCount++
  1331.         next
  1332.     }
  1333.     Bytes_i = SC_i+1
  1334.     }
  1335.     Object = UnWeb(CGIstrip($URL_i),1)
  1336.     Host = $Host_i
  1337.  
  1338.     if (!Common())
  1339.     next
  1340.  
  1341.     if (Counting) {
  1342.     ETime = EntryTime()
  1343.     if (DayFracs)
  1344.         DayFracLog[int(ETime%86400/BucketSecs)]++
  1345.     if (DoDayOfWeek)
  1346.         # Jan 1 1969 was a Wednesday.  Add 3 to make that be at index 3.
  1347.         DayOfWeekLog[(int(ETime/86400)+3)%7]++
  1348.     if (Interval) {
  1349.         IntLog[int(ETime/Interval)]++
  1350.         if (Debug)
  1351.         printf "Incrementing bucket %d\n",int(ETime/Interval) \
  1352.         > "/dev/stderr"
  1353.     }
  1354.     next
  1355.     }
  1356.     if (!CountHosts) {
  1357.     if (Debug > 2 && !(Index in Count))
  1358.         printf "New index (total now %d): %s\n",
  1359.         ++numInd,Uncontrol(Index) > "/dev/stderr"
  1360.     Count[Index]++
  1361.     }
  1362.     else if (!((Object,Host) in TrackPairs)) {
  1363.     TrackPairs[Object,Host]
  1364.     Count[Index]++
  1365.     }
  1366.     else if (Debug)
  1367.     printf "Already saw %s accessed from %s\n",Uncontrol(Object),
  1368.     Uncontrol(Host) > "/dev/stderr"
  1369.     EndDate = Date()
  1370.     if (AllAccesses)
  1371.     Accesses[Object] = Accesses[Object] "\n\t" EndDate " " Host
  1372.     if (TrackLast)
  1373.     LastXferDate[Index] = EndDate
  1374.     if (ChkRaw)
  1375.     Log(Object,MaybeRaw,Reports,RawSet,$0)
  1376.     if (LogBytes && $Bytes_i ~ /^[0-9]+$/)
  1377.     Bytes[Index] += $Bytes_i
  1378.     next
  1379. }
  1380.  
  1381. Debug {
  1382.     printf \
  1383. "Record %d skipped; not enough fields or wrong method.\n"\
  1384. "%d fields; method index = %d; method = <%s>.  Line follows:\n%s\n",
  1385.     NR,NF,Op_i,Uncontrol($Op_i),Uncontrol($0) > "/dev/stderr"
  1386.     WrongNumFields++
  1387. }
  1388.  
  1389. ##### End of logging functions.
  1390. ##### Start of report generating functions.
  1391.  
  1392. END {
  1393.     if (Err != "")
  1394.     exit Err
  1395.     if (Progress && NR >= 1000)
  1396.     printf "\r%d\n",NR > "/dev/stderr"
  1397.     if (Debug) {
  1398.     print "Finished reading input." > "/dev/stderr"
  1399.     printf \
  1400. "Records skipped:\n"\
  1401. "%6d header/trailer lines.\n"\
  1402. "%6d failed transfer records.\n"\
  1403. "%6d host/object pattern reject.\n"\
  1404. "%6d wrong number of fields or uncounted method field.\n"\
  1405. "%6d status field not found.\n",
  1406.     NotRecs,HTTPFailCount,NotMatch,WrongNumFields,NoStatus > "/dev/stderr"
  1407.     }
  1408.     if (Counting) {
  1409.     if (Interval) {
  1410.         PrintIntervalData()
  1411.         ReptPrinted = 1
  1412.     }
  1413.     if (DayFracs) {
  1414.         if (ReptPrinted)
  1415.         print ""
  1416.         PrintBuckets(DayFracLog,DayFracs-1,BucketSecs,"time of day",
  1417.         FirstTime,LastTime)
  1418.         ReptPrinted = 1
  1419.     }
  1420.     if (DoDayOfWeek) {
  1421.         if (ReptPrinted)
  1422.         print ""
  1423.         PrintBuckets(DayOfWeekLog,6,86400,"day of week",FirstTime,LastTime)
  1424.     }
  1425.     exit(0)
  1426.     }
  1427.     # Sort all objects by access count once, so that it doesn't have to be
  1428.     # done for each user.
  1429.     if (Debug)
  1430.     print "Sorting..." > "/dev/stderr"
  1431.     if (LogBytes && SortBytes)
  1432.     Num = qsortArbIndByValue(Bytes,k)
  1433.     else
  1434.     Num = qsortArbIndByValue(Count,k)
  1435.     if (Debug)
  1436.     printf "Sorted %d object(s).\n",Num > "/dev/stderr"
  1437.     if (ReportAll) {
  1438.     CopySet(LevelSet,NotRawSet)
  1439.     SubtractSet(NotRawSet,RawSet)
  1440.     delete NotRawSet["none"]
  1441.     if (Num > 0) {
  1442.         # Generate the non-raw reports to be sent to users.
  1443.         for (i = Num; i >= 1; i--) {
  1444.         Object = k[i]
  1445.         Log(Object,MaybeNotRaw,Reports,NotRawSet,i)
  1446.         if (LogBytes && (Object in Bytes) && Bytes[Object] > MaxBytes)
  1447.             MaxBytes = Bytes[Object]
  1448.         }
  1449.         # Mail reports to users.
  1450.         GenReports(UserLevelSet,MailNames,Count[k[Num]],MaxBytes,UseSubmit,
  1451.         Sender)
  1452.     }
  1453.     }
  1454.     else {
  1455.     if (Num > 0) {
  1456.         if (LogBytes)
  1457.         for (i = 1; i <= Num; i++)
  1458.             if (k[i] in Bytes && (Bytes[k[i]]+0) > MaxBytes)
  1459.             MaxBytes = b
  1460.         Header = MkHeader(StartDate,EndDate,Width,Count[k[Num]],MaxBytes,
  1461.         TrackLast)
  1462.         if (Debug)
  1463.         printf "Got header: %s\n",Header > "/dev/stderr"
  1464.         if (DoHeader) {
  1465.         # The ObjPattern stuff is here mainly so the feature wherein
  1466.         # the user's name is included in the title can be partially
  1467.         # tested without -r
  1468.         if (ObjPattern ~ "\\^/~[a-z][-a-z0-9]+/$") {
  1469.             User = substr(ObjPattern,4)
  1470.             sub("/","",User)
  1471.         }
  1472.         else
  1473.             User = ""
  1474.         print Title(StartDate,EndDate,HTMLout,User)
  1475.         print Header
  1476.         }
  1477.         Total = 0
  1478.         if (MostAccessed) {
  1479.         for (i = Num; i >= 1; i--) {
  1480.             Object = k[i]
  1481.             if (SStart = match(Object,"./")) {
  1482.             # Truncate before the second /
  1483.             BaseDir = substr(Object,1,SStart)
  1484.             if (!(BaseDir in BaseDirs)) {
  1485.                 BaseDirs[BaseDir]
  1486.                 print FormatLine(i,HTMLout,AllAccesses,TrackLast)
  1487.             }
  1488.             }
  1489.         }
  1490.         }
  1491.         else if (CountOnly)
  1492.         for (i = Num; i >= 1; i--)
  1493.             FormatLine(i)
  1494.         else
  1495.         for (i = Num; i >= 1; i--)
  1496.             print FormatLine(i,HTMLout,AllAccesses,TrackLast)
  1497.         if (Debug)
  1498.         printf "Done with object records.\n" > "/dev/stderr"
  1499.         if (!("b" in Options))
  1500.         print Trailer(CountLen,Total,NR - Total - NotRecs,TotalBytes,
  1501.         HTMLout)
  1502.     }
  1503.     else
  1504.         print "No http transfers."
  1505.     }
  1506. }
  1507.  
  1508. function PrintBuckets(Data,MaxBucket,BucketPeriod,PeriodName,FirstTime,LastTime,
  1509.   i) {
  1510.     printf "Accesses by %s, from %s to %s\n",
  1511.     PeriodName,strftime("%y/%m/%d %T",FirstTime-TZOffset),
  1512.     strftime("%y/%m/%d %T",LastTime-TZOffset)
  1513.     for (i = 0; i <= MaxBucket; i++)
  1514.     printf "%d %d\n",i,Data[i]
  1515. }
  1516.  
  1517. # Globals: Interval, FirstTime, LastTime, IntLog[]
  1518. function PrintIntervalData(  i,LTime) {
  1519.     printf "Accesses for each interval of %d seconds,\nfrom %s to %s\n",
  1520.     Interval,strftime("%y/%m/%d %T",FirstTime-TZOffset),
  1521.     strftime("%y/%m/%d %T",LastTime-TZOffset)
  1522.     LTime = int(LastTime/Interval)
  1523.     for (i = int(FirstTime/Interval); i <= LTime; i++)
  1524.     printf "%s %d\n",strftime("%y/%m/%d %T",i*Interval-TZOffset),IntLog[i]
  1525. }
  1526.  
  1527. # Format & return the object whose information is stored at index i
  1528. # Also adds its count to Total & its byte count to TotalBytes
  1529. # HTML: Format line as HTML
  1530. # Globals used:
  1531. # Format, TotWidth, Count[], LastXferDate[], LastRemote[], Total,
  1532. # URLpref
  1533. function FormatLine(i,HTML,AllAccesses,TrackLast,  Object,s) {
  1534.     Object = k[i]
  1535.     if (Debug > 2)
  1536.     printf "Formatting line for object: %s\n",
  1537.     Uncontrol(Object) > "/dev/stderr"
  1538.     Total += Count[Object]
  1539.     if (LogBytes && Object in Bytes)
  1540.     TotalBytes += Bytes[Object]
  1541.     if (HTML)
  1542.     s = sprintf(Format,Count[Object],
  1543.     TrackLast ? LastXferDate[Object] : "",
  1544.     TrackLast ? LastRemote[Object] : "",
  1545.     LogBytes ? (Object in Bytes ? sprintf("%.18g",Bytes[Object]) : "-") \
  1546.     : "",
  1547.     "<a href=" URLpref Object ">" Object "</a>")
  1548.     else
  1549.     s = substr(sprintf(Format,Count[Object],
  1550.     TrackLast ? LastXferDate[Object] : "",
  1551.     TrackLast ? LastRemote[Object] : "",
  1552.     LogBytes ? (Object in Bytes ? sprintf("%.18g",Bytes[Object]) : "-") \
  1553.     : "",Object),
  1554.     1,TotWidth)
  1555.     if (AllAccesses)
  1556.     s = s Accesses[Object]
  1557.     return s
  1558. }
  1559.  
  1560. # Variables:
  1561. # StartDate, EndDate: start and end of log, in string form.
  1562. # Width: line length.
  1563. # MaxNum: Highest count for any object.
  1564. # MaxBytes: Highest byte count for any object.
  1565. # Return value: A format string for printing data lines.
  1566. # Global variables:
  1567. # TotWidth is set to the max line length.
  1568. # CountLen is set to the width of the Count field.
  1569. function MkHeader(StartDate,EndDate,Width,MaxNum,MaxBytes,TrackLast,
  1570. BytesFmt,BytesLen,RemoteLen) {
  1571.     CountLen = max(length(sprintf("%s",MaxNum)),3)
  1572.     if (LogBytes) {
  1573.     BytesLen = max(length(sprintf("%.18g",MaxBytes)),5)
  1574.     BytesFmt = "%" BytesLen "s "
  1575.     BytesLen++
  1576.     if (Debug)
  1577.         printf "Maximum byte value: %.18g\n",MaxBytes > "/dev/stderr"
  1578.     }
  1579.     else
  1580.     BytesFmt = "%.0s"
  1581.     if (Width == 0)
  1582.     RemoteLen = 24
  1583.     else {
  1584.     RemoteLen = Width - 55 - BytesLen
  1585.     if (RemoteLen < 0)
  1586.         RemoteLen = 0
  1587.     }
  1588.     if (TrackLast)
  1589.     Format = \
  1590.     "%" CountLen "s %12s  %-" RemoteLen "." RemoteLen "s " BytesFmt "%s"
  1591.     else
  1592.     Format = "%" CountLen "s%.0s%.0s " BytesFmt "%s"
  1593.     if (Width == 0)
  1594.     RemoteLen = TotWidth = 1000
  1595.     else
  1596.     TotWidth = Width
  1597.  
  1598.     if (Debug)
  1599.     printf "Total width: %s\n",TotWidth > "/dev/stderr"
  1600.     if (TrackObjects)
  1601.     return sprintf(Format,"Num","Last Xfer","Last Host","Bytes","Object")
  1602.     else
  1603.     return sprintf(Format,"Num","Last Xfer","Host","Bytes","Last Object")
  1604. }
  1605.  
  1606. # Emit HTML ASCII title (not column headers).
  1607. function Title(StartDate,EndDate,HTML,User,  hdr,URL,ttl) {
  1608.     # This won't be correct in cases where a user is sent a report
  1609.     # that includes info for accesses outside their page, but it's
  1610.     # about the best that can be done.
  1611.     if (User !~ "^[a-z][-a-z0-9]+$")
  1612.     User = ""
  1613.     hdr = sprintf("HTTP accesses%s, %s to %s",
  1614.     (User != "") ? " for " User : "",StartDate,EndDate)
  1615.     if (HTML) {
  1616.     ttl = (User == "") ? hdr : \
  1617.     sprintf("HTTP accesses for <a href=" URLpref "/~" User "/>" \
  1618.     User "</a>, %s to %s",StartDate,EndDate)
  1619.     return "<HTML>\n<HEAD>\n<title>" hdr "</title>\n</HEAD>\n<BODY>\n"\
  1620.     "<h2>" ttl "</h2><pre>"
  1621.     }
  1622.     else
  1623.     return hdr
  1624. }
  1625.  
  1626. function Trailer(CountLen,TotalAcc,Ignored,TotalBytes,HTML,  Bytes,iInfo) {
  1627.     if (Debug && LogBytes)
  1628.     printf "%.18g bytes xferred.  Generating trailer...\n",
  1629.     TotalBytes > "/dev/stderr"
  1630.     if (LogBytes)
  1631.     Bytes = sprintf(" (%sB transferred)",i2emet(TotalBytes,6,1,0,1))
  1632.     if (Ignored >= 0)
  1633.     iInfo = sprintf("; %d records skipped.",Ignored)
  1634.     if (HTML)
  1635.     return \
  1636.     sprintf("</pre>\n<hr><h1>Total Accesses: %d%s%s</h1>\n</BODY>\n</HTML>",
  1637.     TotalAcc,Bytes,iInfo)
  1638.     else 
  1639.     return sprintf("%*d Total Accesses%s%s",CountLen,TotalAcc,Bytes,iInfo)
  1640. }
  1641.  
  1642. # Input variables:
  1643. # Levs[1..n]: The report levels to check whether a user wants.
  1644. # Addr: The name of the user to generate a level list for.
  1645. # ReptLevels[] contains a User,Level index for each report level that a user
  1646. #     requested.
  1647. # Output variables:
  1648. # LevSet[]: The set of levels requested by the user.
  1649. # Return value: space-separated list of report levels requested by user.
  1650. function MakeLevList(Levs,LevSet,Addr,ReptLevels,  LevNum,LevList,Level) {
  1651.     for (LevNum = 1; LevNum in Levs; LevNum++) {
  1652.     Level = Levs[LevNum]
  1653.     if (Level != "none" && (Addr,Level) in ReptLevels) {
  1654.         LevList = LevList " " Level
  1655.         LevSet[Level]
  1656.     }
  1657.     }
  1658.     return substr(LevList,2)
  1659. }
  1660.  
  1661. function InitReportMail(recipient,subject,Fields,UseSubmit,Sender,
  1662. To,Cc,Bcc,Order,SubmitOpts) {
  1663. #    return sprintf("mail -s '%s' %s",subject,recipient)
  1664.     To[1] = recipient
  1665.     Fields["From"] = Sender " (HTTP Report Daemon)"
  1666.     SubmitOpts["t"]    # trust author specification
  1667.     SubmitOpts["q"]    # do not return undelivered mail
  1668.     SubmitOpts["z"]    # do not warn about undelivered mail
  1669.     return InitMail(To,Cc,Bcc,Fields,Order,subject,Sender,UseSubmit,SubmitOpts)
  1670. }
  1671.  
  1672. # Globals used:
  1673. # Width, LevelOrder, StartDate, EndDate, Width, Reports[], Test, Debug,
  1674. # Options, Subject, Total, TotalBytes
  1675. function GenReports(ReptLevels,MailNames,MaxNum,MaxBytes,UseSubmit,Sender,
  1676. i,Level,NumLines,Addr,Levs,LevNum,Header,Line,Cmd,AccLine,testMailAddr,HTMLout,
  1677. DidTitle,LevSet,ExtraHeaders,LevList,AllAcc,HeaderOrder) {
  1678.     split(LevelOrder,Levs,",")
  1679.     testMailAddr = "M" in Options
  1680.     # For each user who is to get a report
  1681.     for (Addr in MailNames) {
  1682.     # If html is the only level requested, add appropriate headers.
  1683.     if ((LevList = MakeLevList(Levs,LevSet,Addr,ReptLevels)) == "html") {
  1684.         ExtraHeaders["MIME-Version"] = "1.0"
  1685.         ExtraHeaders["Content-Type"] = "text/html; charset=us-ascii"
  1686.         HeaderOrder[1] = "MIME-Version"
  1687.         HeaderOrder[2] = "Content-Type"
  1688.     }
  1689.     else
  1690.         split("",ExtraHeaders)
  1691.         ExtraHeaders["X-HTTPReport-Levels"] = LevList
  1692.     if (!Test) {
  1693.         Cmd = InitReportMail(testMailAddr ? Options["M"] : Addr,
  1694.         Subject (testMailAddr ? " [for " Addr "]" : ""),ExtraHeaders,
  1695.         UseSubmit,Sender)
  1696.     }
  1697.     DidTitle = 0
  1698.     # For each level that can be requested
  1699.     for (LevNum = 1; LevNum in Levs; LevNum++) {
  1700.         if ((Level = Levs[LevNum]) == "none")
  1701.         continue
  1702.         HTMLout = (Level == "html")
  1703.         # If the user is to get a report at this level...
  1704.         if ((Addr,Level) in ReptLevels) {
  1705.         AllAcc = (Level == "allhits")
  1706.  
  1707.         # "b" option means to not print a header.
  1708.         # We want a header to be printed at the start of the reports,
  1709.         # and also want an HTML format header if an HTML report was
  1710.         # requested.
  1711.         if ((!DidTitle || HTMLout) && !Test && !("b" in Options)) {
  1712.             AccLine = Title(StartDate,EndDate,HTMLout,Addr)
  1713.             if (Debug)
  1714.             print AccLine > "/dev/stderr"
  1715.             print AccLine | Cmd
  1716.             DidTitle = 1
  1717.         }
  1718.  
  1719.         if (Debug)
  1720.             printf "%s report for %s:\n",Level,Addr > "/dev/stderr"
  1721.         NumLines = ReptLevels[Addr,Level]
  1722.         if (Level == "raw")
  1723.             for (i = 1; i <= NumLines; i++) {
  1724.             Line = Reports[Addr,Level,i]
  1725.             if (Debug)
  1726.                 print Line > "/dev/stderr"
  1727.             if (!Test)
  1728.                 print Line | Cmd
  1729.             }
  1730.         else {
  1731.             Total = 0
  1732.             TotalBytes = 0
  1733.             Header = MkHeader(StartDate,EndDate,
  1734.             (Level == "wide" || HTMLout) ? 0 : Width,MaxNum,MaxBytes,
  1735.             !AllAcc)
  1736.             if (!Test && !("b" in Options))
  1737.             print Header | Cmd
  1738.             if (Debug)
  1739.             print Header > "/dev/stderr"
  1740.             for (i = 1; i <= NumLines; i++) {
  1741.             Line = FormatLine(Reports[Addr,Level,i],HTMLout,AllAcc,
  1742.             !AllAcc)
  1743.             if (Debug)
  1744.                 print Line > "/dev/stderr"
  1745.             if (!Test)
  1746.                 print Line | Cmd
  1747.             }
  1748.             if (!Test && !("b" in Options))
  1749.             print Trailer(CountLen,Total,-1,TotalBytes,HTMLout) | \
  1750.             Cmd
  1751.         }
  1752.         if (Debug)
  1753.             print "" > "/dev/stderr"
  1754.         if (!Test)
  1755.             print "" | Cmd
  1756.         }
  1757.         else if (Debug)
  1758.         printf "%s report for %s: none.\n",Level,Addr > "/dev/stderr"
  1759.     }
  1760.     if (!Test)
  1761.         close(Cmd)
  1762.     }
  1763. }
  1764.  
  1765. function max(a,b) {
  1766.     if (a > b)
  1767.     return a
  1768.     else
  1769.     return b
  1770. }
  1771.  
  1772. # Deletes any elements that are in both Minuend and Subtrahend from Minuend.
  1773. function SubtractSet(Minuend,Subtrahend,  Elem) {
  1774.     for (Elem in Subtrahend)
  1775.     delete Minuend[Elem]
  1776. }
  1777.  
  1778. function CopySet(From,To,  Elem) {
  1779.     for (Elem in From)
  1780.     To[Elem]
  1781. }
  1782.  
  1783. function Intersection(A,B,Inter,  Elem,Count) {
  1784.     for (Elem in A)
  1785.     if (Elem in B) {
  1786.         Inter[Elem]
  1787.         Count++
  1788.     }
  1789.     return Count
  1790. }
  1791.  
  1792. # @(#) CmdReadLine 95/09/04
  1793. # Run Command, read a single line of output from it, then close it.
  1794. # If Verbose is true, a complaint is issued if the read fails.
  1795. # Output is returned in $*
  1796. # The return value from getline is returned.  It will be 1 on a successful
  1797. # read; 0 if no lines were read due because the command produced no output
  1798. # or could not be run.  ERRNO is never set since pipes are run by a shell.
  1799. function CmdReadLine(Command,Verbose,  ret) {
  1800.     if (Debug) {
  1801.     print "* Issuing command: " Command "\n"\
  1802.           "* Waiting for single line of output..." > "/dev/stderr"
  1803.     }
  1804.     ret = Command | getline
  1805.     if (Verbose && ret != 1)
  1806.     printf "Read from pipe '%s' failed\n",Command
  1807.     # close doesn't return a value under awk, only gawk
  1808.     close(Command)
  1809.     if (Debug)
  1810.     print "* Output: " $0 > "/dev/stderr"
  1811.     return ret
  1812. }
  1813. ### Begin utty,id routines
  1814.  
  1815. # utty: find ttys a user is logged in on.
  1816. # For each tty User is logged in on, an element is created in TTYs[].
  1817. # The index is the name of the tty, with a leading "/dev/".
  1818. # The value is set to 1 if the user is writable on that tty, 0 if not.
  1819. # The number of ttys the user is logged in on is returned.
  1820. function utty(User,TTYs,  Cmd,Count) {
  1821.     Cmd = "exec who -T"
  1822.     Count = 0
  1823.     while ((Cmd | getline) == 1)
  1824.     if ($1 == User) {
  1825.         if ($2 == "+")
  1826.         TTYs[$3] = 1
  1827.         else
  1828.         TTYs[$3] = 0
  1829.         Count++
  1830.     }
  1831.     close(Cmd)
  1832.     return Count
  1833. }
  1834.  
  1835. # id returns the user name of the user who owns the current process.
  1836. # In the array IDs, elements are set as follows:
  1837. # uid: numeric user id
  1838. # gid: numeric group id
  1839. # group: group name, if any
  1840. # user: user name, if any
  1841. function id(IDs,  Cmd,line,elem) {
  1842.     Cmd = "exec id"
  1843.     Cmd | getline line
  1844.     split(line,elem,"[()=]")
  1845.     close(Cmd)
  1846.     IDs["user"] = elem[3]
  1847.     IDs["gid"] = elem[5]
  1848.     IDs["group"] = elem[6]
  1849.     return IDs["uid"] = elem[2]
  1850. }
  1851.  
  1852. ### End utty,id routines
  1853. ### Start of ProcArgs library
  1854. # @(#) ProcArgs 1.11 96/12/08
  1855. # 92/02/29 john h. dubois iii (john@armory.com)
  1856. # 93/07/18 Added "#" arg type
  1857. # 93/09/26 Do not count -h against MinArgs
  1858. # 94/01/01 Stop scanning at first non-option arg.  Added ">" option type.
  1859. #          Removed meaning of "+" or "-" by itself.
  1860. # 94/03/08 Added & option and *()< option types.
  1861. # 94/04/02 Added NoRCopt to Opts()
  1862. # 94/06/11 Mark numeric variables as such.
  1863. # 94/07/08 Opts(): Do not require any args if h option is given.
  1864. # 95/01/22 Record options given more than once.  Record option num in argv.
  1865. # 95/06/08 Added ExclusiveOptions().
  1866. # 96/01/20 Let rcfiles be a colon-separated list of filenames.
  1867. #          Expand $VARNAME at the start of its filenames.
  1868. #          Let varname=0 and -option- turn off an option.
  1869. # 96/05/05 Changed meaning of 7th arg to Opts; now can specify exactly how many
  1870. #          of the vars should be searched for in the environment.
  1871. #          Check for duplicate rcfiles.
  1872. # 96/05/13 Return more specific error values.  Note: ProcArgs() and InitOpts()
  1873. #          now return various negatives values on error, not just -1, and
  1874. #          Opts() may set Err to various positive values, not just 1.
  1875. #          Added AllowUnrecOpt.
  1876. # 96/05/23 Check type given for & option
  1877. # 96/06/15 Re-port to awk
  1878. # 96/10/01 Moved file-reading code into ReadConfFile(), so that it can be
  1879. #          used by other functions.
  1880. # 96/10/15 Added OptChars
  1881. # 96/11/01 Added exOpts arg to Opts()
  1882. # 96/11/16 Added ; type
  1883. # 96/12/08 Added Opt2Set() & Opt2Sets()
  1884. # 96/12/27 Added CmdLineOpt()
  1885.  
  1886. # optlist is a string which contains all of the possible command line options.
  1887. # A character followed by certain characters indicates that the option takes
  1888. # an argument, with type as follows:
  1889. # :    String argument
  1890. # ;    Non-empty string argument
  1891. # *    Floating point argument
  1892. # (    Non-negative floating point argument
  1893. # )    Positive floating point argument
  1894. # #    Integer argument
  1895. # <    Non-negative integer argument
  1896. # >    Positive integer argument
  1897. # The only difference the type of argument makes is in the runtime argument
  1898. # error checking that is done.
  1899.  
  1900. # The & option is a special case used to get numeric options without the
  1901. # user having to give an option character.  It is shorthand for [-+.0-9].
  1902. # If & is included in optlist and an option string that begins with one of
  1903. # these characters is seen, the value given to "&" will include the first
  1904. # char of the option.  & must be followed by a type character other than ":"
  1905. # or ";".
  1906. # Note that if e.g. &> is given, an option of -.5 will produce an error.
  1907.  
  1908. # Strings in argv[] which begin with "-" or "+" are taken to be
  1909. # strings of options, except that a string which consists solely of "-"
  1910. # or "+" is taken to be a non-option string; like other non-option strings,
  1911. # it stops the scanning of argv and is left in argv[].
  1912. # An argument of "--" or "++" also stops the scanning of argv[] but is removed.
  1913. # If an option takes an argument, the argument may either immediately
  1914. # follow it or be given separately.
  1915. # "-" and "+" options are treated the same.  "+" is allowed because most awks
  1916. # take any -options to be arguments to themselves.  gawk 2.15 was enhanced to
  1917. # stop scanning when it encounters an unrecognized option, though until 2.15.5
  1918. # this feature had a flaw that caused problems in some cases.  See the OptChars
  1919. # parameter to explicitly set the option-specifier characters.
  1920.  
  1921. # If an option that does not take an argument is given,
  1922. # an index with its name is created in Options and its value is set to the
  1923. # number of times it occurs in argv[].
  1924.  
  1925. # If an option that does take an argument is given, an index with its name is
  1926. # created in Options and its value is set to the value of the argument given
  1927. # for it, and Options[option-name,"count"] is (initially) set to the 1.
  1928. # If an option that takes an argument is given more than once,
  1929. # Options[option-name,"count"] is incremented, and the value is assigned to
  1930. # the index (option-name,instance) where instance is 2 for the second occurance
  1931. # of the option, etc.
  1932. # In other words, the first time an option with a value is encountered, the
  1933. # value is assigned to an index consisting only of its name; for any further
  1934. # occurances of the option, the value index has an extra (count) dimension.
  1935.  
  1936. # The sequence number for each option found in argv[] is stored in
  1937. # Options[option-name,"num",instance], where instance is 1 for the first
  1938. # occurance of the option, etc.  The sequence number starts at 1 and is
  1939. # incremented for each option, both those that have a value and those that
  1940. # do not.  Options set from a config file have a value of 0 assigned to this.
  1941.  
  1942. # Options and their arguments are deleted from argv.
  1943. # Note that this means that there may be gaps left in the indices of argv[].
  1944. # If compress is nonzero, argv[] is packed by moving its elements so that
  1945. # they have contiguous integer indices starting with 0.
  1946. # Option processing will stop with the first unrecognized option, just as
  1947. # though -- was given except that unlike -- the unrecognized option will not be
  1948. # removed from ARGV[].  Normally, an error value is returned in this case.
  1949. # If AllowUnrecOpt is true, it is not an error for an unrecognized option to
  1950. # be found, so the number of remaining arguments is returned instead.
  1951. # If OptChars is not a null string, it is the set of characters that indicate
  1952. # that an argument is an option string if the string begins with one of the
  1953. # characters.  A string consisting solely of two of the same option-indicator
  1954. # characters stops the scanning of argv[].  The default is "-+".
  1955. # argv[0] is not examined.
  1956. # The number of arguments left in argc is returned.
  1957. # If an error occurs, the global string OptErr is set to an error message
  1958. # and a negative value is returned.
  1959. # Current error values:
  1960. # -1: option that required an argument did not get it.
  1961. # -2: argument of incorrect type supplied for an option.
  1962. # -3: unrecognized (invalid) option.
  1963. function ProcArgs(argc,argv,OptList,Options,compress,AllowUnrecOpt,OptChars,
  1964. ArgNum,ArgsLeft,Arg,ArgLen,ArgInd,Option,Pos,NumOpt,Value,HadValue,specGiven,
  1965. NeedNextOpt,GotValue,OptionNum,Escape,dest,src,count,c,OptTerm,OptCharSet)
  1966. {
  1967. # ArgNum is the index of the argument being processed.
  1968. # ArgsLeft is the number of arguments left in argv.
  1969. # Arg is the argument being processed.
  1970. # ArgLen is the length of the argument being processed.
  1971. # ArgInd is the position of the character in Arg being processed.
  1972. # Option is the character in Arg being processed.
  1973. # Pos is the position in OptList of the option being processed.
  1974. # NumOpt is true if a numeric option may be given.
  1975.     ArgsLeft = argc
  1976.     NumOpt = index(OptList,"&")
  1977.     OptionNum = 0
  1978.     if (OptChars == "")
  1979.     OptChars = "-+"
  1980.     while (OptChars != "") {
  1981.     c = substr(OptChars,1,1)
  1982.     OptChars = substr(OptChars,2)
  1983.     OptCharSet[c]
  1984.     OptTerm[c c]
  1985.     }
  1986.     for (ArgNum = 1; ArgNum < argc; ArgNum++) {
  1987.     Arg = argv[ArgNum]
  1988.     if (length(Arg) < 2 || !((specGiven = substr(Arg,1,1)) in OptCharSet))
  1989.         break    # Not an option; quit
  1990.     if (Arg in OptTerm) {
  1991.         delete argv[ArgNum]
  1992.         ArgsLeft--
  1993.         break
  1994.     }
  1995.     ArgLen = length(Arg)
  1996.     for (ArgInd = 2; ArgInd <= ArgLen; ArgInd++) {
  1997.         Option = substr(Arg,ArgInd,1)
  1998.         if (NumOpt && Option ~ /[-+.0-9]/) {
  1999.         # If this option is a numeric option, make its flag be & and
  2000.         # its option string flag position be the position of & in
  2001.         # the option string.
  2002.         Option = "&"
  2003.         Pos = NumOpt
  2004.         # Prefix Arg with a char so that ArgInd will point to the
  2005.         # first char of the numeric option.
  2006.         Arg = "&" Arg
  2007.         ArgLen++
  2008.         }
  2009.         # Find position of flag in option string, to get its type (if any).
  2010.         # Disallow & as literal flag.
  2011.         else if (!(Pos = index(OptList,Option)) || Option == "&") {
  2012.         if (AllowUnrecOpt) {
  2013.             Escape = 1
  2014.             break
  2015.         }
  2016.         else {
  2017.             OptErr = "Invalid option: " specGiven Option
  2018.             return -3
  2019.         }
  2020.         }
  2021.  
  2022.         # Find what the value of the option will be if it takes one.
  2023.         # NeedNextOpt is true if the option specifier is the last char of
  2024.         # this arg, which means that if the option requires a value it is
  2025.         # the next arg.
  2026.         if (NeedNextOpt = (ArgInd >= ArgLen)) { # Value is the next arg
  2027.         if (GotValue = ArgNum + 1 < argc)
  2028.             Value = argv[ArgNum+1]
  2029.         }
  2030.         else {    # Value is included with option
  2031.         Value = substr(Arg,ArgInd + 1)
  2032.         GotValue = 1
  2033.         }
  2034.  
  2035.         if (HadValue = AssignVal(Option,Value,Options,
  2036.         substr(OptList,Pos + 1,1),GotValue,"",++OptionNum,!NeedNextOpt,
  2037.         specGiven)) {
  2038.         if (HadValue < 0)    # error occured
  2039.             return HadValue
  2040.         if (HadValue == 2)
  2041.             ArgInd++    # Account for the single-char value we used.
  2042.         else {
  2043.             if (NeedNextOpt) {    # option took next arg as value
  2044.             delete argv[++ArgNum]
  2045.             ArgsLeft--
  2046.             }
  2047.             break    # This option has been used up
  2048.         }
  2049.         }
  2050.     }
  2051.     if (Escape)
  2052.         break
  2053.     # Do not delete arg until after processing of it, so that if it is not
  2054.     # recognized it can be left in ARGV[].
  2055.     delete argv[ArgNum]
  2056.     ArgsLeft--
  2057.     }
  2058.     if (compress != 0) {
  2059.     dest = 1
  2060.     src = argc - ArgsLeft + 1
  2061.     for (count = ArgsLeft - 1; count; count--) {
  2062.         ARGV[dest] = ARGV[src]
  2063.         dest++
  2064.         src++
  2065.     }
  2066.     }
  2067.     return ArgsLeft
  2068. }
  2069.  
  2070. # Assignment to values in Options[] occurs only in this function.
  2071. # Option: Option specifier character.
  2072. # Value: Value to be assigned to option, if it takes a value.
  2073. # Options[]: Options array to return values in.
  2074. # ArgType: Argument type specifier character.
  2075. # GotValue: Whether any value is available to be assigned to this option.
  2076. # Name: Name of option being processed.
  2077. # OptionNum: Number of this option (starting with 1) if set in argv[],
  2078. #     or 0 if it was given in a config file or in the environment.
  2079. # SingleOpt: true if the value (if any) that is available for this option was
  2080. #     given as part of the same command line arg as the option.  Used only for
  2081. #     options from the command line.
  2082. # specGiven is the option specifier character use, if any (e.g. - or +),
  2083. # for use in error messages.
  2084. # Global variables: OptErr
  2085. # Return value: negative value on error, 0 if option did not require an
  2086. # argument, 1 if it did & used the whole arg, 2 if it required just one char of
  2087. # the arg.
  2088. # Current error values:
  2089. # -1: Option that required an argument did not get it.
  2090. # -2: Value of incorrect type supplied for option.
  2091. # -3: Bad type given for option &
  2092. function AssignVal(Option,Value,Options,ArgType,GotValue,Name,OptionNum,
  2093. SingleOpt,specGiven,  UsedValue,Err,NumTypes) {
  2094.     # If option takes a value...    [
  2095.     NumTypes = "*()#<>]"
  2096.     if (Option == "&" && ArgType !~ "[" NumTypes) {    # ]
  2097.     OptErr = "Bad type given for & option"
  2098.     return -3
  2099.     }
  2100.  
  2101.     if (UsedValue = (ArgType ~ "[:;" NumTypes)) {    # ]
  2102.     if (!GotValue) {
  2103.         if (Name != "")
  2104.         OptErr = "Variable requires a value -- " Name
  2105.         else
  2106.         OptErr = "option requires an argument -- " Option
  2107.         return -1
  2108.     }
  2109.     if ((Err = CheckType(ArgType,Value,Option,Name,specGiven)) != "") {
  2110.         OptErr = Err
  2111.         return -2
  2112.     }
  2113.     # Mark this as a numeric variable; will be propogated to Options[] val.
  2114.     if (ArgType != ":" && ArgType != ";")
  2115.         Value += 0
  2116.     if ((Instance = ++Options[Option,"count"]) > 1)
  2117.         Options[Option,Instance] = Value
  2118.     else
  2119.         Options[Option] = Value
  2120.     }
  2121.     # If this is an environ or rcfile assignment & it was given a value...
  2122.     else if (!OptionNum && Value != "") {
  2123.     UsedValue = 1
  2124.     # If the value is "0" or "-" and this is the first instance of it,
  2125.     # do not set Options[Option]; this allows an assignment in an rcfile to
  2126.     # turn off an option (for the simple "Option in Options" test) in such
  2127.     # a way that it cannot be turned on in a later file.
  2128.     if (!(Option in Options) && (Value == "0" || Value == "-"))
  2129.         Instance = 1
  2130.     else
  2131.         Instance = ++Options[Option]
  2132.     # Save the value even though this is a flag
  2133.     Options[Option,Instance] = Value
  2134.     }
  2135.     # If this is a command line flag and has a - following it in the same arg,
  2136.     # it is being turned off.
  2137.     else if (OptionNum && SingleOpt && substr(Value,1,1) == "-") {
  2138.     UsedValue = 2
  2139.     if (Option in Options)
  2140.         Instance = ++Options[Option]
  2141.     else
  2142.         Instance = 1
  2143.     Options[Option,Instance]
  2144.     }
  2145.     # If this is a flag assignment without a value, increment the count for the
  2146.     # flag unless it was turned off.  The indicator for a flag being turned off
  2147.     # is that the flag index has not been set in Options[] but it has an
  2148.     # instance count.
  2149.     else if (Option in Options || !((Option,1) in Options))
  2150.     # Increment number of times this flag seen; will inc null value to 1
  2151.     Instance = ++Options[Option]
  2152.     Options[Option,"num",Instance] = OptionNum
  2153.     return UsedValue
  2154. }
  2155.  
  2156. # Option is the option letter
  2157. # Value is the value being assigned
  2158. # Name is the var name of the option, if any
  2159. # ArgType is one of:
  2160. # :    String argument
  2161. # ;    Non-null string argument
  2162. # *    Floating point argument
  2163. # (    Non-negative floating point argument
  2164. # )    Positive floating point argument
  2165. # #    Integer argument
  2166. # <    Non-negative integer argument
  2167. # >    Positive integer argument
  2168. # specGiven is the option specifier character use, if any (e.g. - or +),
  2169. # for use in error messages.
  2170. # Returns null on success, err string on error
  2171. function CheckType(ArgType,Value,Option,Name,specGiven,  Err,ErrStr) {
  2172.     if (ArgType == ":")
  2173.     return ""
  2174.     if (ArgType == ";") {
  2175.     if (Value == "")
  2176.         Err = "must be a non-empty string"
  2177.     }
  2178.     # A number begins with optional + or -, and is followed by a string of
  2179.     # digits or a decimal with digits before it, after it, or both
  2180.     else if (Value !~ /^[-+]?([0-9]+|[0-9]*\.[0-9]+|[0-9]+\.)$/)
  2181.     Err = "must be a number"
  2182.     else if (ArgType ~ "[#<>]" && Value ~ /\./)
  2183.     Err = "may not include a fraction"
  2184.     else if (ArgType ~ "[()<>]" && Value < 0)
  2185.     Err = "may not be negative"
  2186.     # (
  2187.     else if (ArgType ~ "[)>]" && Value == 0)
  2188.     Err = "must be a positive number"
  2189.     if (Err != "") {
  2190.     ErrStr = "Bad value \"" Value "\".  Value assigned to "
  2191.     if (Name != "")
  2192.         return ErrStr "variable " substr(Name,1,1) " " Err
  2193.     else {
  2194.         if (Option == "&")
  2195.         Option = Value
  2196.         return ErrStr "option " specGiven substr(Option,1,1) " " Err
  2197.     }
  2198.     }
  2199.     else
  2200.     return ""
  2201. }
  2202.  
  2203. # Note: only the above functions are needed by ProcArgs.
  2204. # The rest of these functions call ProcArgs() and also do other
  2205. # option-processing stuff.
  2206.  
  2207. # Opts: Process command line arguments.
  2208. # Opts processes command line arguments using ProcArgs()
  2209. # and checks for errors.  If an error occurs, a message is printed
  2210. # and the program is exited.
  2211. #
  2212. # Input variables:
  2213. # Name is the name of the program, for error messages.
  2214. # Usage is a usage message, for error messages.
  2215. # OptList the option description string, as used by ProcArgs().
  2216. # MinArgs is the minimum number of non-option arguments that this
  2217. # program should have, non including ARGV[0] and +h.
  2218. # If the program does not require any non-option arguments,
  2219. # MinArgs should be omitted or given as 0.
  2220. # rcFiles, if given, is a colon-seprated list of filenames to read for
  2221. # variable initialization.  If a filename begins with ~/, the ~ is replaced
  2222. # by the value of the environment variable HOME.  If a filename begins with
  2223. # $, the part from the character after the $ up until (but not including)
  2224. # the first character not in [a-zA-Z0-9_] will be searched for in the
  2225. # environment; if found its value will be substituted, if not the filename will
  2226. # be discarded.
  2227. # rcfiles are read in the order given.
  2228. # Values given in them will not override values given on the command line,
  2229. # and values given in later files will not override those set in earlier
  2230. # files, because AssignVal() will store each with a different instance index.
  2231. # The first instance of each variable, either on the command line or in an
  2232. # rcfile, will be stored with no instance index, and this is the value
  2233. # normally used by programs that call this function.
  2234. # VarNames is a comma-separated list of variable names to map to options,
  2235. # in the same order as the options are given in OptList.
  2236. # If EnvSearch is given and nonzero, the first EnvSearch variables will also be
  2237. # searched for in the environment.  If set to -1, all values will be searched
  2238. # for in the environment.  Values given in the environment will override
  2239. # those given in the rcfiles but not those given on the command line.
  2240. # NoRCopt, if given, is an additional letter option that if given on the
  2241. # command line prevents the rcfiles from being read.
  2242. # See ProcArgs() for a description of AllowUnRecOpt and optChars, and
  2243. # ExclusiveOptions() for a description of exOpts.
  2244. # Special options:
  2245. # If x is made an option and is given, some debugging info is output.
  2246. # h is assumed to be the help option.
  2247.  
  2248. # Global variables:
  2249. # The command line arguments are taken from ARGV[].
  2250. # The arguments that are option specifiers and values are removed from
  2251. # ARGV[], leaving only ARGV[0] and the non-option arguments.
  2252. # The number of elements in ARGV[] should be in ARGC.
  2253. # After processing, ARGC is set to the number of elements left in ARGV[].
  2254. # The option values are put in Options[].
  2255. # On error, Err is set to a positive integer value so it can be checked for in
  2256. # an END block.
  2257. # Return value: The number of elements left in ARGV is returned.
  2258. # Must keep OptErr global since it may be set by InitOpts().
  2259. function Opts(Name,Usage,OptList,MinArgs,rcFiles,VarNames,EnvSearch,NoRCopt,
  2260. AllowUnrecOpt,optChars,exOpts,  ArgsLeft,e) {
  2261.     if (MinArgs == "")
  2262.     MinArgs = 0
  2263.     ArgsLeft = ProcArgs(ARGC,ARGV,OptList NoRCopt,Options,1,AllowUnrecOpt,
  2264.     optChars)
  2265.     if (ArgsLeft < (MinArgs+1) && !("h" in Options)) {
  2266.     if (ArgsLeft >= 0) {
  2267.         OptErr = "Not enough arguments"
  2268.         Err = 4
  2269.     }
  2270.     else
  2271.         Err = -ArgsLeft
  2272.     printf "%s: %s.\nUse -h for help.\n%s\n",
  2273.     Name,OptErr,Usage > "/dev/stderr"
  2274.     exit 1
  2275.     }
  2276.     if (rcFiles != "" && (NoRCopt == "" || !(NoRCopt in Options)) &&
  2277.     (e = InitOpts(rcFiles,Options,OptList,VarNames,EnvSearch)) < 0)
  2278.     {
  2279.     print Name ": " OptErr ".\nUse -h for help." > "/dev/stderr"
  2280.     Err = -e
  2281.     exit 1
  2282.     }
  2283.     if ((exOpts != "") && ((OptErr = ExclusiveOptions(exOpts,Options)) != ""))
  2284.     {
  2285.     printf "%s: Error: %s\n",Name,OptErr > "/dev/stderr"
  2286.     Err = 1
  2287.     exit 1
  2288.     }
  2289.     return ArgsLeft
  2290. }
  2291.  
  2292. # ReadConfFile(): Read a file containing var/value assignments, in the form
  2293. # <variable-name><assignment-char><value>.
  2294. # Whitespace (spaces and tabs) around a variable (leading whitespace on the
  2295. # line and whitespace between the variable name and the assignment character) 
  2296. # is stripped.  Lines that do not contain an assignment operator or which
  2297. # contain a null variable name are ignored, other than possibly being noted in
  2298. # the return value.  If more than one assignment is made to a variable, the
  2299. # first assignment is used.
  2300. # Input variables:
  2301. # File is the file to read.
  2302. # Comment is the line-comment character.  If it is found as the first non-
  2303. #     whitespace character on a line, the line is ignored.
  2304. # Assign is the assignment string.  The first instance of Assign on a line
  2305. #     separates the variable name from its value.
  2306. # If StripWhite is true, whitespace around the value (whitespace between the
  2307. #     assignment char and trailing whitespace on the line) is stripped.
  2308. # VarPat is a pattern that variable names must match.  
  2309. #     Example: "^[a-zA-Z][a-zA-Z0-9]+$"
  2310. # If FlagsOK is true, variables are allowed to be "set" by being put alone on
  2311. #     a line; no assignment operator is needed.  These variables are set in
  2312. #     the output array with a null value.  Lines containing nothing but
  2313. #     whitespace are still ignored.
  2314. # Output variables:
  2315. # Values[] contains the assignments, with the indexes being the variable names
  2316. #     and the values being the assigned values.
  2317. # Lines[] contains the line number that each variable occured on.  A flag set
  2318. #     is record by giving it an index in Lines[] but not in Values[].
  2319. # Return value:
  2320. # If any errors occur, a string consisting of descriptions of the errors
  2321. # separated by newlines is returned.  In no case will the string start with a
  2322. # numeric value.  If no errors occur,  the number of lines read is returned.
  2323. function ReadConfigFile(Values,Lines,File,Comment,Assign,StripWhite,VarPat,
  2324. FlagsOK,
  2325. Line,Status,Errs,AssignLen,LineNum,Var,Val) {
  2326.     if (Comment != "")
  2327.     Comment = "^" Comment
  2328.     AssignLen = length(Assign)
  2329.     if (VarPat == "")
  2330.     VarPat = "."    # null varname not allowed
  2331.     while ((Status = (getline Line < File)) == 1) {
  2332.     LineNum++
  2333.     sub("^[ \t]+","",Line)
  2334.     if (Line == "")        # blank line
  2335.         continue
  2336.     if (Comment != "" && Line ~ Comment)
  2337.         continue
  2338.     if (Pos = index(Line,Assign)) {
  2339.         Var = substr(Line,1,Pos-1)
  2340.         Val = substr(Line,Pos+AssignLen)
  2341.         if (StripWhite) {
  2342.         sub("^[ \t]+","",Val)
  2343.         sub("[ \t]+$","",Val)
  2344.         }
  2345.     }
  2346.     else {
  2347.         Var = Line    # If no value, var is entire line
  2348.         Val = ""
  2349.     }
  2350.     if (!FlagsOK && Val == "") {
  2351.         Errs = Errs \
  2352.         sprintf("\nBad assignment on line %d of file %s: %s",
  2353.         LineNum,File,Line)
  2354.         continue
  2355.     }
  2356.     sub("[ \t]+$","",Var)
  2357.     if (Var !~ VarPat) {
  2358.         Errs = Errs sprintf("\nBad variable name on line %d of file %s: %s",
  2359.         LineNum,File,Var)
  2360.         continue
  2361.     }
  2362.     if (!(Var in Lines)) {
  2363.         Lines[Var] = LineNum
  2364.         if (Pos)
  2365.         Values[Var] = Val
  2366.     }
  2367.     }
  2368.     if (Status)
  2369.     Errs = Errs "\nCould not read file " File
  2370.     close(File)
  2371.     return Errs == "" ? LineNum : substr(Errs,2)    # Skip first newline
  2372. }
  2373.  
  2374. # Variables:
  2375. # Data is stored in Options[].
  2376. # rcFiles, OptList, VarNames, and EnvSearch are as as described for Opts().
  2377. # Global vars:
  2378. # Sets OptErr.  Uses ENVIRON[].
  2379. # If anything is read from any of the rcfiles, sets READ_RCFILE to 1.
  2380. function InitOpts(rcFiles,Options,OptList,VarNames,EnvSearch,
  2381. Line,Var,Pos,Vars,Map,CharOpt,NumVars,TypesInd,Types,Type,Ret,i,rcFile,
  2382. fNames,numrcFiles,filesRead,Err,Values,retStr) {
  2383.     split("",filesRead,"")    # make awk know this is an array
  2384.     NumVars = split(VarNames,Vars,",")
  2385.     TypesInd = Ret = 0
  2386.     if (EnvSearch == -1)
  2387.     EnvSearch = NumVars
  2388.     for (i = 1; i <= NumVars; i++) {
  2389.     Var = Vars[i]
  2390.     CharOpt = substr(OptList,++TypesInd,1)
  2391.     if (CharOpt ~ "^[:;*()#<>&]$")
  2392.         CharOpt = substr(OptList,++TypesInd,1)
  2393.     Map[Var] = CharOpt
  2394.     Types[Var] = Type = substr(OptList,TypesInd+1,1)
  2395.     # Do not overwrite entries from environment
  2396.     if (i <= EnvSearch && Var in ENVIRON &&
  2397.     (Err = AssignVal(CharOpt,ENVIRON[Var],Options,Type,1,Var,0)) < 0)
  2398.         return Err
  2399.     }
  2400.  
  2401.     numrcFiles = split(rcFiles,fNames,":")
  2402.     for (i = 1; i <= numrcFiles; i++) {
  2403.     rcFile = fNames[i]
  2404.     if (rcFile ~ "^~/")
  2405.         rcFile = ENVIRON["HOME"] substr(rcFile,2)
  2406.     else if (rcFile ~ /^\$/) {
  2407.         rcFile = substr(rcFile,2)
  2408.         match(rcFile,"^[a-zA-Z0-9_]*")
  2409.         envvar = substr(rcFile,1,RLENGTH)
  2410.         if (envvar in ENVIRON)
  2411.         rcFile = ENVIRON[envvar] substr(rcFile,RLENGTH+1)
  2412.         else
  2413.         continue
  2414.     }
  2415.     if (rcFile in filesRead)
  2416.         continue
  2417.     # rcfiles are liable to be given more than once, e.g. UHOME and HOME
  2418.     # may be the same
  2419.     filesRead[rcFile]
  2420.     if ("x" in Options)
  2421.         printf "Reading configuration file %s\n",rcFile > "/dev/stderr"
  2422.     retStr = ReadConfigFile(Values,Lines,rcFile,"#","=",0,"",1)
  2423.     if (retStr > 0)
  2424.         READ_RCFILE = 1
  2425.     else if (ret != "") {
  2426.         OptErr = retStr
  2427.         Ret = -1
  2428.     }
  2429.     for (Var in Lines)
  2430.         if (Var in Map) {
  2431.         if ((Err = AssignVal(Map[Var],
  2432.         Var in Values ? Values[Var] : "",Options,Types[Var],
  2433.         Var in Values,Var,0)) < 0)
  2434.             return Err
  2435.         }
  2436.         else {
  2437.         OptErr = sprintf(\
  2438.         "Unknown var \"%s\" assigned to on line %d\nof file %s",Var,
  2439.         Lines[Var],rcFile)
  2440.         Ret = -1
  2441.         }
  2442.     }
  2443.  
  2444.     if ("x" in Options)
  2445.     for (Var in Map)
  2446.         if (Map[Var] in Options)
  2447.         printf "(%s) %s=%s\n",Map[Var],Var,Options[Map[Var]] > \
  2448.         "/dev/stderr"
  2449.         else
  2450.         printf "(%s) %s not set\n",Map[Var],Var > "/dev/stderr"
  2451.     return Ret
  2452. }
  2453.  
  2454. # OptSets is a semicolon-separated list of sets of option sets.
  2455. # Within a list of option sets, the option sets are separated by commas.  For
  2456. # each set of sets, if any option in one of the sets is in Options[] AND any
  2457. # option in one of the other sets is in Options[], an error string is returned.
  2458. # If no conflicts are found, nothing is returned.
  2459. # Example: if OptSets = "ab,def,g;i,j", an error will be returned due to
  2460. # the exclusions presented by the first set of sets (ab,def,g) if:
  2461. # (a or b is in Options[]) AND (d, e, or f is in Options[]) OR
  2462. # (a or b is in Options[]) AND (g is in Options) OR
  2463. # (d, e, or f is in Options[]) AND (g is in Options)
  2464. # An error will be returned due to the exclusions presented by the second set
  2465. # of sets (i,j) if: (i is in Options[]) AND (j is in Options[]).
  2466. # todo: make options given on command line unset options given in config file
  2467. # todo: that they conflict with.
  2468. function ExclusiveOptions(OptSets,Options,
  2469. Sets,SetSet,NumSets,Pos1,Pos2,Len,s1,s2,c1,c2,ErrStr,L1,L2,SetSets,NumSetSets,
  2470. SetNum,OSetNum) {
  2471.     NumSetSets = split(OptSets,SetSets,";")
  2472.     # For each set of sets...
  2473.     for (SetSet = 1; SetSet <= NumSetSets; SetSet++) {
  2474.     # NumSets is the number of sets in this set of sets.
  2475.     NumSets = split(SetSets[SetSet],Sets,",")
  2476.     # For each set in a set of sets except the last...
  2477.     for (SetNum = 1; SetNum < NumSets; SetNum++) {
  2478.         s1 = Sets[SetNum]
  2479.         L1 = length(s1)
  2480.         for (Pos1 = 1; Pos1 <= L1; Pos1++)
  2481.         # If any of the options in this set was given, check whether
  2482.         # any of the options in the other sets was given.  Only check
  2483.         # later sets since earlier sets will have already been checked
  2484.         # against this set.
  2485.         if ((c1 = substr(s1,Pos1,1)) in Options)
  2486.             for (OSetNum = SetNum+1; OSetNum <= NumSets; OSetNum++) {
  2487.             s2 = Sets[OSetNum]
  2488.             L2 = length(s2)
  2489.             for (Pos2 = 1; Pos2 <= L2; Pos2++)
  2490.                 if ((c2 = substr(s2,Pos2,1)) in Options)
  2491.                 ErrStr = ErrStr "\n"\
  2492.                 sprintf("Cannot give both %s and %s options.",
  2493.                 c1,c2)
  2494.             }
  2495.     }
  2496.     }
  2497.     if (ErrStr != "")
  2498.     return substr(ErrStr,2)
  2499.     return ""
  2500. }
  2501.  
  2502. # The value of each instance of option Opt that occurs in Options[] is made an
  2503. # index of Set[].
  2504. # The return value is the number of instances of Opt in Options.
  2505. function Opt2Set(Options,Opt,Set,  count) {
  2506.     if (!(Opt in Options))
  2507.     return 0
  2508.     Set[Options[Opt]]
  2509.     count = Options[Opt,"count"]
  2510.     for (; count > 1; count--)
  2511.     Set[Options[Opt,count]]
  2512.     return count
  2513. }
  2514.  
  2515. # The value of each instance of option Opt that occurs in Options[] that
  2516. # begins with "!" is made an index of nSet[] (with the ! stripped from it).
  2517. # Other values are made indexes of Set[].
  2518. # The return value is the number of instances of Opt in Options.
  2519. function Opt2Sets(Options,Opt,Set,nSet,  count,aSet,ret) {
  2520.     ret = Opt2Set(Options,Opt,aSet)
  2521.     for (value in aSet)
  2522.     if (substr(value,1,1) == "!")
  2523.         nSet[substr(value,2)]
  2524.     else
  2525.         Set[value]
  2526.     return ret
  2527. }
  2528.  
  2529. # Returns true if option Opt was given on the command line.
  2530. function CmdLineOpt(Options,Opt,  i) {
  2531.     for (i = 1; (Opt,"num",i) in Options; i++)
  2532.     if (Options[Opt,"num",i] != 0)
  2533.         return 1
  2534.     return 0
  2535. }
  2536. ### End of ProcArgs library
  2537. # @(#) i2met.awk 1.0 96/02/13
  2538. # jhdiii 96/01/14
  2539. # Convert positive integer value Value to a string at most MaxLen characters
  2540. # long.  This is done by converting the integer to a string of the form n*m,
  2541. # where m is a metric suffix from: K M G
  2542. # If Pow2 is true, then each factor of 1K is taken to be 1024; if it is false,
  2543. # it is taken to be 1000.
  2544. # MaxLen must be between 4 and 9.
  2545. # Value may be any integer from 0..maxint
  2546. # If Units is given, it is the units that Value is passed in, where
  2547. # Units=1 means Value is in K; Units=2 means Value is in M, etc.
  2548. function i2met(Value,MaxLen,Pow2,Units,  Len,Div) {
  2549.     if (Value == 0)
  2550.     return "0"
  2551.     if (!(1 in Suf))
  2552.     split("K,M,G,T,P,E,Z,Y",Suf,",")
  2553.     # In both awk & gawk, integer values that can be represented as
  2554.     # machine integers will be printed as integers.
  2555.     # If value can be printed without modification, return it as it,
  2556.     # but with a multiplier suffix if reqd.
  2557.     if ((Len = length(Value Suf[Units])) <= MaxLen)
  2558.     return Value Suf[Units]
  2559.     MaxLen -= 1        # Leave space for suffix
  2560.     Div = Pow2 ? 1024 : 1000
  2561.     for (Units += 1; Units in Suf; Units++)
  2562.     if (length(int(Value /= Div)) <= MaxLen)
  2563.         break
  2564.     Value = substr(sprintf("%." MaxLen "f",Value),1,MaxLen)
  2565.     if (substr(Value,MaxLen,1) == ".")
  2566.     Value = substr(Value,1,MaxLen-1)
  2567.     return Value Suf[Units]
  2568. }
  2569.  
  2570. # @(#) i2emet.awk 1.0 96/02/13
  2571. # jhdiii 96/01/27
  2572. # Convert numeric value Value to one with the decimal point set according to
  2573. # engineering convention.  In this convention, there is always between 1 and
  2574. # 3 digits before the decimal point.  A metric suffix is attached to retain
  2575. # the original value.
  2576. # If Pow2 is true, then each factor of 1K is taken to be 1024; if it is false,
  2577. # it is taken to be 1000.
  2578. # If Pow2 is true, MaxLen must be >= 5; if Pow2 is false, MaxLen must be >= 4.
  2579. # If Units is given, it is the units that Value is passed in, where
  2580. # Units=0 means Value is in base units; Units=-1 means Value is in milliunits,
  2581. # Units=1 means Value is in kilounits, etc.
  2582. # If NoZeroes is true, trailing zeroes in the fractional part are removed.
  2583. function i2emet(Value,MaxLen,Pow2,Units,NoZeroes,  Len,Factor,i,suf2) {
  2584.     if (Value == 0)
  2585.     return "0"
  2586.     if (!(1 in _Suf)) {
  2587.     _MaxUnit = split("K,M,G,T,P,E,Z,Y",_Suf,",")
  2588.     split("m,u,n,p,f,a,z,y",suf2,",")
  2589.     for (i = 1; i in suf2; i++)
  2590.         _Suf[-i] = suf2[i]
  2591.     }
  2592.     # Make sure awk treats all of these as numbers
  2593.     Factor = (Pow2 ? 1024 : 1000)+0
  2594.     Units += 0
  2595.     Value += 0
  2596.     if (Value < 1)
  2597.     for (; Value < 1 && Units > -_MaxUnit; Value *= Factor)
  2598.         Units--
  2599.     else
  2600.     for (; Value >= Factor && Units < _MaxUnit; Value /= Factor)
  2601.         Units++
  2602.     if (Units)
  2603.     MaxLen -= 1        # Leave space for suffix
  2604.     # Round reasonably carefully
  2605.     fDig = MaxLen-length(int(Value))-1
  2606.     if (fDig > 0)
  2607.     Value = sprintf("%." fDig "f",Value)+0    # Turn it back into a number!
  2608.     else
  2609.     Value = int(Value+0.5)
  2610.     # Rounding may have caused rollover of leading digit, making the result
  2611.     # exceed the allowed range (e.g. 999.6 -> 1000)
  2612.     if (Value >= Factor) {
  2613.     Value /= Factor
  2614.     Units++
  2615.     }
  2616.     if (substr(Value,MaxLen,1) == ".")
  2617.     Value = substr(Value,1,MaxLen-1)    # Get rid of trailing "."
  2618.     else
  2619.     Value = substr(Value,1,MaxLen)
  2620.     if (NoZeroes && Value ~ /\..*0$/)
  2621.     sub(/\.?0+$/,"",Value)
  2622.     return Value _Suf[Units]
  2623. }
  2624. ### Begin qsort routines
  2625.  
  2626. # Arr[] is an array of values with arbitrary indices.
  2627. # k[] is returned with numeric indices 1..n.
  2628. # The values in k[] are the indices of Arr[],
  2629. # ordered so that if Arr[] is stepped through
  2630. # in the order Arr[k[1]] .. Arr[k[n]], it will be stepped
  2631. # through in order of the values of its elements.
  2632. # The return value is the number of elements in the arrays (n).
  2633. function qsortArbIndByValue(Arr,k,  ArrInd,ElNum) {
  2634.     ElNum = 0
  2635.     for (ArrInd in Arr)
  2636.     k[++ElNum] = ArrInd
  2637.     qsortSegment(Arr,k,1,ElNum)
  2638.     return ElNum
  2639. }
  2640.  
  2641. # Sort a segment of an array.
  2642. # Arr[] contains data with arbitrary indices.
  2643. # k[] has indices 1..nelem, with the indices of arr[] as values.
  2644. # This function sorts the elements of arr that are pointed to by
  2645. # k[start..end], swapping the values of elements of k[] so that
  2646. # when this function returns arr[k[start..end]] will be in order.
  2647. function qsortSegment(Arr,k,start,end,  left,right,sepval,tmp,tmpe,tmps) {
  2648.     # handle two-element case explicitly for a tiny speedup
  2649.     if ((end - start) == 1) {
  2650.     if (Arr[tmps = k[start]] > Arr[tmpe = k[end]]) {
  2651.         k[start] = tmpe
  2652.         k[end] = tmps
  2653.     }
  2654.     return
  2655.     }
  2656.     # Make sure comparisons act on these as numbers
  2657.     left = start+0
  2658.     right = end+0
  2659.     sepval = Arr[k[int((left + right) / 2)]]
  2660.     # Make every element <= sepval be to the left of every element > sepval
  2661.     while (left < right) {
  2662.     while (Arr[k[left]] < sepval)
  2663.         left++
  2664.     while (Arr[k[right]] > sepval)
  2665.         right--
  2666.     if (left < right) {
  2667.         tmp = k[left]
  2668.         k[left++] = k[right]
  2669.         k[right--] = tmp
  2670.     }
  2671.     }
  2672.     if (left == right)
  2673.     if (Arr[k[left]] < sepval)
  2674.         left++
  2675.     else
  2676.         right--
  2677.     if (start < right)
  2678.     qsortSegment(Arr,k,start,right)
  2679.     if (left < end)
  2680.     qsortSegment(Arr,k,left,end)
  2681. }
  2682.  
  2683. # Arr[] is an array of values with arbitrary indices.
  2684. # k[] is returned with numeric indices 1..n.
  2685. # The values in k are the indices of Arr[],
  2686. # ordered so that if Arr[] is stepped through
  2687. # in the order Arr[k[1]] .. Arr[k[n]], it will be stepped
  2688. # through in order of the values of its indices.
  2689. # The return value is the number of elements in the arrays (n).
  2690. # If the indexes are numeric, Numeric should be true, so that they can be
  2691. # compared as such rather than as strings.  Numeric indexes do not have to be
  2692. # contiguous.
  2693. function qsortByArbIndex(Arr,k,Numeric,  ArrInd,ElNum) {
  2694.     ElNum = 0
  2695.     if (Numeric)
  2696.     # Indexes do not preserve numeric type, so must be forced
  2697.     for (ArrInd in Arr)
  2698.         k[++ElNum] = ArrInd+0
  2699.     else
  2700.     for (ArrInd in Arr)
  2701.         k[++ElNum] = ArrInd
  2702.     qsortNumIndByValue(k,1,ElNum)
  2703.     return ElNum
  2704. }
  2705.  
  2706. # Arr is an array of elements with contiguous numeric indexes to be sorted
  2707. # by value.
  2708. # start and end are the starting and ending indexes of the range to be sorted.
  2709. function qsortNumIndByValue(Arr,start,end,  left,right,sepval,tmp,tmpe,tmps) {
  2710.     # handle two-element case explicitly for a tiny speedup
  2711.     if ((start - end) == 1) {
  2712.     if ((tmps = Arr[start]) > (tmpe = Arr[end])) {
  2713.         Arr[start] = tmpe
  2714.         Arr[end] = tmps
  2715.     }
  2716.     return
  2717.     }
  2718.     left = start+0
  2719.     right = end+0
  2720.     sepval = Arr[int((left + right) / 2)]
  2721.     while (left < right) {
  2722.     while (Arr[left] < sepval)
  2723.         left++
  2724.     while (Arr[right] > sepval)
  2725.         right--
  2726.     if (left <= right) {
  2727.         tmp = Arr[left]
  2728.         Arr[left++] = Arr[right]
  2729.         Arr[right--] = tmp
  2730.     }
  2731.     }
  2732.     if (start < right)
  2733.     qsortNumIndByValue(Arr,start,right)
  2734.     if (left < end)
  2735.     qsortNumIndByValue(Arr,left,end)
  2736. }
  2737.  
  2738. ### End qsort routines
  2739. ### Begin UnControl routines
  2740.  
  2741. # @(#) uncontrol.awk 1.1 96/05/29
  2742. # 92/11/09 john h. dubois iii (john@armory.com)
  2743. # 96/05/29 Added octal-only conversion.
  2744.  
  2745. # Uncontrol(S): Convert control characters in S to symbolic form.
  2746. # Characters in S with values < 32 and with value 127 are converted to the form
  2747. # ^X.  Characters with value >= 128 are converted to the octal form \0nnn,
  2748. # where nnn is the octal value of the character.
  2749. # The resulting string is returned.
  2750. # If OctalOnly is true, octal numbers are used for all symbolic values instead
  2751. # of ^X.
  2752. # Global variables: UncTable[] and char2octal[].
  2753. function Uncontrol(S,OctalOnly,  i,len,Output) {
  2754.     len = length(S)
  2755.     Output = ""
  2756.     if (!("a" in UncTable))
  2757.     MakeUncontrolTable()
  2758.     for (i = 1; i <= len; i++)
  2759.     Output = Output \
  2760.     (OctalOnly ? char2octal[substr(S,i,1)] : UncTable[substr(S,i,1)])
  2761.     return Output
  2762. }
  2763.  
  2764. # MakeUncontrolTable: Make tables for use by Uncontrol().
  2765. # Global variables:
  2766. # UncTable[] is made into a character -> symbolic character lookup table
  2767. # with characters with values < 32 and with value 127 converted to the form
  2768. # ^X, and characters with value >= 128 are converted to the octal form \0nnn.
  2769. # char2octal[] is made into a similar table but with all non-printing chars
  2770. # in the form \0nnn.
  2771. function MakeUncontrolTable(  i,c) {
  2772.     for (i = 0; i < 32; i++) {
  2773.     UncTable[c = sprintf("%c",i)] = "^" sprintf("%c",i + 64)
  2774.     char2octal[c] = "\\" sprintf("%03o",i)
  2775.     }
  2776.     for (i = 32; i < 127; i++) {
  2777.     c = sprintf("%c",i)
  2778.     char2octal[c]  = UncTable[c] = sprintf("%c",i)
  2779.     }
  2780.     UncTable[c = sprintf("%c",127)] = "^?"
  2781.     char2octal[c] = "\\0177"
  2782.     for (i = 128; i < 256; i++) {
  2783.     UncTable[c = sprintf("%c",i)] = "\\" sprintf("%03o",i)
  2784.     char2octal[c] = "\\" sprintf("%03o",i)
  2785.     }
  2786. }
  2787.  
  2788. ### End UnControl routines
  2789. function UnWeb(Value,HexOnly,  UnWebbed) {
  2790.     # Do + first, since % conversion may yield plusses
  2791.     if (!HexOnly)
  2792.     gsub(/\+/," ",Value)
  2793.     while (match(Value,/%[a-fA-F0-9][a-fA-F0-9]/)) {
  2794.     UnWebbed = UnWebbed substr(Value,1,RSTART-1) \
  2795.     sprintf("%c",strtoi(substr(Value,RSTART+1,2),16))
  2796.     Value = substr(Value,RSTART+3)
  2797.     }
  2798.     UnWebbed = UnWebbed Value
  2799.     return UnWebbed
  2800. }
  2801. # @(#) strtol 1.0 96/03/01
  2802. # 96/03/01 john h. dubois iii (john@armory.com)
  2803.  
  2804. # Convert a value in base Base to an integer.
  2805. function strtoi(S,Base,  ret,len,i,conv,digit) {
  2806.     if (Base < 2 || Base > 36)
  2807.     return ""
  2808.     S = tolower(S)
  2809.     len = length(S)
  2810.     conv = substr("0123456789abcdefghijklmnopqrstuvwxyz",1,Base)
  2811.     for (i = 1; i <= len; i++) {
  2812.     if (!(digit = index(conv,substr(S,i,1))))
  2813.         return ""
  2814.     ret = ret * Base + digit - 1
  2815.     }
  2816.     return ret
  2817. }
  2818.  
  2819. # If Base is 1-36, S is taken to be a number in base Base.
  2820. # If Base is 16, an initial 0x or 0X is ignored.
  2821. # If Base is 0, an initial 0x or 0X causes Base to be set to 16; otherwise
  2822. # Base is set to 10.
  2823. # If S is empty or contains any characters not appropriate to a number in
  2824. # base Base, a null string is returned.  On success, an integer value is
  2825. # returned.
  2826. function strtol(S,Base) {
  2827.     Base += 0    # yes, this is neccessary
  2828.     if (Base < 0 || Base > 36)
  2829.     return ""
  2830.     if (Base == 0)
  2831.     if (S ~ /^0[xX]/) {
  2832.         Base = 16
  2833.         S = substr(S,3)
  2834.     }
  2835.     else
  2836.         Base = 10
  2837.     else if (Base == 16 && S ~ /^0[xX]/)
  2838.     S = substr(S,3)
  2839.     return strtoi(S,Base)
  2840. }
  2841.  
  2842. ### Start of mail sending routines.
  2843. # @(#) mail-send.gawk 2.0 97/02/22
  2844. # 96/01/29 john h. dubois iii (john@armory.com)
  2845. # 97/02/15 Rewritten.
  2846. #
  2847. # Returns name of cmd to pipe into, ready for body of message.
  2848. # To[], Cc[], Fields[], and Order[] are as described for header822()
  2849. # Bcc[] is an additional list of recipients who should not be mentioned in
  2850. # headers.
  2851. # If a non-null Subject is passed, as a convenience it is added to Fields[]
  2852. # before it is passed to header822().
  2853. # If a non-null From is passed, it is used as the return address.  It does
  2854. # not affect the headers unless the MTA records it in them.
  2855. function InitMail(To,Cc,Bcc,Fields,Order,Subject,From,UseSubmit,SubmitOpts,
  2856. Debug,  Recips,i,j,Cmd) {
  2857.     for (i = 1; i in To; i++)
  2858.     Recips[++j] = To[i]
  2859.     for (i = 1; i in Cc; i++)
  2860.     Recips[++j] = Cc[i]
  2861.     for (i = 1; i in Bcc; i++)
  2862.     Recips[++j] = Bcc[i]
  2863.     if (UseSubmit)
  2864.     Cmd = InitSubmit(Recips,From,SubmitOpts,Debug)
  2865.     else
  2866.     Cmd = InitSendmail(Recips,From)
  2867.     if (Cmd ~ "^!")
  2868.     return Cmd
  2869.     if (Subject != "")
  2870.     Fields["Subject"] = Subject
  2871.     printf "%s\n",header822(Fields,To,Cc,Order) | Cmd
  2872.     return Cmd
  2873. }
  2874.  
  2875. # Sets up globals _ParamSOpts[] and _NoParamSOpts[] for use as sets of 
  2876. # submit options to do and do not take values.
  2877. function SubmitParam(Opt,Val,  i,c) {
  2878.     if (!("r" in _NoParamSOpts)) {
  2879.     for (i = 1; (c = substr("Wcdhjlmnqrstuvwz",i,1)) != ""; i++)
  2880.         _NoParamSOpts[c]
  2881.     for (i = 1; (c = substr("LUVfghikx",i,1)) != ""; i++)
  2882.         _ParamSOpts[c]
  2883.     }
  2884.     if (Opt in _ParamSOpts)
  2885.     return Opt Val "*"
  2886.     else if (Opt in _NoParamSOpts)
  2887.     return Opt
  2888.     else
  2889.     return ""
  2890. }
  2891.  
  2892. # Return value: Command to pipe into.  If an invalid submit option is passed,
  2893. # a null string is returned.
  2894. # Submit options are:
  2895. # i*    Source channel
  2896. # h*    Source host
  2897. # t    Trust Sender/From line (root/mmdf only)
  2898. # u    Don't trust Sender/From line (add Source-Info line to header)
  2899. # f*    Don't trust Sender/From line; add given text
  2900. # x*    Extract recipient list from named fields (comma-separated list)
  2901. #       RecipList[] will not be used if this option is given.
  2902. # g*    Extract recipient list from named fields and use explicit list
  2903. # v    Report validity of each address given, rather than aborting on any bad
  2904. # m    Deliver to mailbox (default; there used to be a tty option as well).
  2905. #       m is always turned on for the sake of old versions.
  2906. # l    Deliver local mail immediately; overrides mod=reg in MMDF config
  2907. # n    Deliver netmail immediately; overrides mod=reg in MMDF config
  2908. # w    Watch immediate delivery attempts
  2909. # r    Return undelivered mail to submitter when it expires
  2910. # s    Return undelivered mail to address given by 'Sender:' when it expires
  2911. # q    Do not return undelivered mail (discard it when it expires).
  2912. #       Remote systems may still return mail.  Use q with null From parameter
  2913. #     for no returns at all.
  2914. # c    If mail is returned, include only a citation of the contents
  2915. # z    Do not send warnings re undelivered (but not expired) mail
  2916. # d    Don't use delay channel.  If 1st nameserver use fails, mail is returned
  2917. # j    Used by the delay channel to indicate that submission is by it
  2918. # k*    Specify nameserver timeout.  Parameter is in seconds.
  2919. # W    Watch submission.  Output is sent to fd 2
  2920. # L*    Specify logfile (root/mmdf only)
  2921. # V*    Specify logging level (root/mmdf only). FAT TMP GEN BST FST PTR BTR FTR
  2922. # U*    Specify invoker's UID (root only)
  2923. function InitSubmit(RecipList,From,SubmitOpts,Debug,  Cmd,i,c,Opt) {
  2924.     # If not special return handling requested and no return address given,
  2925.     # send returns to submitter
  2926.     if (From == "" && !("s" in SubmitOpts || "q" in SubmitOpts))
  2927.     SubmitOpts["r"]
  2928.     SubmitOpts["m"]
  2929.     for (Opt in SubmitOpts)
  2930.     if ((c = SubmitParam(Opt,SubmitOpts[Opt])) == "")
  2931.         return "!Error initializing submit: Bad option '" Opt "'."
  2932.     else
  2933.         Cmd = Cmd c
  2934.     if (Cmd != "")
  2935.     Cmd = " -" Cmd
  2936.     Cmd = "exec /usr/mmdf/bin/submit" Cmd
  2937.     if (Debug) {
  2938.     Cmd = "exec tee /dev/tty | " Cmd
  2939.     print "mail submission command: " Cmd > "/dev/stderr"
  2940.     }
  2941.     if (!("r" in SubmitOpts || "s" in SubmitOpts))
  2942.     print From | Cmd    # Explicit return address - maybe empty
  2943.     if (!("x" in SubmitOpts)) {    # If explicit addresses may be given
  2944.     for (i = 1; i in RecipList; i++)
  2945.         print RecipList[i] | Cmd
  2946.     print "!" | Cmd    # terminate recipient list
  2947.     }
  2948.     return Cmd
  2949. }
  2950.  
  2951. function InitSendmail(RecipList,From,  ToList,i) {
  2952.     if (From != "")
  2953.     From = " -f '" From "'"
  2954.     for (i = 1; i in RecipList; i++)
  2955.     ToList = ToList " " RecipList[i]
  2956.     return "exec /usr/lib/sendmail" From ToList
  2957. }
  2958.  
  2959. # @(#) GetMailHostName 97/02/12
  2960. # 91/03/13 jhdiii
  2961. # 97/02/12 Use hostname if unable to get name from mmdftailor.
  2962. # Returns the name of the local host that should be used for mail purposes.
  2963. # If mmdftailor is readable and both MLNAME and MLDOMAIN can be found, uses
  2964. # MLNAME.MLDOMAIN.  If not, uses 'hostname'.
  2965. # The name is stored in the global _MailHostName for reuse by this function.
  2966. function GetMailHostName(  mlname,mldomain,proc,tailor,oFS,hostname) {
  2967.     if (_MailHostName != "")
  2968.     return _MailHostName
  2969.     tailor = "/usr/mmdf/mmdftailor"
  2970.     oFS = FS
  2971.     FS = " "    # normal awk field splitting
  2972.     while ((getline < tailor) == 1) {
  2973.         if ($1 == "MLNAME")
  2974.             mlname = $2
  2975.         else if ($1 == "MLDOMAIN")
  2976.             mldomain = $2
  2977.     else
  2978.         continue
  2979.     if (mlname != "" && mldomain != "") {
  2980.         hostname = mlname "." mldomain
  2981.         gsub("\"","",hostname)    # in case values are quoted
  2982.         break
  2983.     }
  2984.     }
  2985.     close(tailor)
  2986.     if (hostname == "") {
  2987.     proc = "/usr/bin/hostname"
  2988.     proc | getline hostname
  2989.     close(proc)
  2990.     }
  2991.     FS = oFS
  2992.     _MailHostName = hostname
  2993.     return hostname
  2994. }
  2995.  
  2996. # Returns an RFC822 recipient field, wrapped as neccessary, ending with a
  2997. # newline.
  2998. function WrapField822(FieldName,Values,
  2999. Field,Line,len,w,i,val,indentLen,indentStr)
  3000. {
  3001.     Line = FieldName ":"
  3002.     len = length(Line)
  3003.     indentLen = len+1
  3004.     for (i = 1; i <= indentLen; i++)
  3005.     indentStr = indentStr " "
  3006.     for (i = 1; i in Values; i++) {
  3007.     val = Values[i]
  3008.     if ((i+1) in Values)
  3009.         val = val ","
  3010.     len += w = length(val)+1
  3011.     if (len > 79) {
  3012.         Field = Field Line "\n"
  3013.         Line = indentStr val
  3014.         len = w + indentLen - 1
  3015.     }
  3016.     else
  3017.         Line = Line " " val
  3018.     }
  3019.     return Field Line "\n"
  3020. }
  3021.  
  3022. # Create an RFC822-compliant mail header.  A blank line is *not* appended;
  3023. # the returned value ends with a single trailing newline.
  3024. # Fields[] contains field values, indexed by name.  The name is given without
  3025. # a trailing :.  To[] and Cc[] are the recipient lists.
  3026. # The first line of the header is the Date: field.  If there is no Date index
  3027. # in Fields[], the current date & time in an RFC822-compliant format is used.
  3028. # The second line of the header is the From: field.  If there is no From index
  3029. # in Fields[], the From: field is made "user@host (name)", where user is the
  3030. # value of the USER environment variable, or the name from 'id' if it is not
  3031. # set; host is as described for GetMailHostName(), and name is from the NAME
  3032. # environment variable.  If NAME is not set, (name) is not included in the
  3033. # From: field.
  3034. # The next lines of the header give the To: and (optionally) Cc: fields.
  3035. # To[] and Cc[] should contain values indexed by integers starting with 1.
  3036. # The fields are built by concatenating these values in the order of their
  3037. # indices.  Header extensions are used as neccessary to keep the length of
  3038. # each physical line below 80 characters if possible.
  3039. # After these are added, any other fields are added.
  3040. # The order they are added in may be specified by assigning the header names to
  3041. # consecutive integer indexes in Order[], starting with 1.
  3042. # Any field named in Order[] that does not exist in Fields[] will not be added.
  3043. # Date, From, To, and Cc should not be given in Order.
  3044. # After all fields named in Order[] are added, any remaining fields are added.
  3045. # Field wrapping is not done to any values in Fields.
  3046. # All elements are removed from Fields[].
  3047. # Minimal RFC822 message has From, Date, and either To or Bcc line.
  3048. function header822(Fields,To,Cc,Order,  header,i,field) {
  3049.     header = "Date: " ( ("Date" in Fields) ? Fields["Date"] :
  3050.     strftime("%a, %d %h %Y %T %Z") ) "\nFrom: "
  3051.     delete Fields["Date"]
  3052.     if ("From" in Fields) {
  3053.     header = header Fields["From"]
  3054.     delete Fields["From"]
  3055.     }
  3056.     else {
  3057.     header = header WhoAmI()
  3058.     header = header "@" GetMailHostName()
  3059.     if ("NAME" in ENVIRON)
  3060.         header = header " (" ENVIRON["NAME"] ")"
  3061.     }
  3062.     header = header "\n" WrapField822("To",To)
  3063.     if (1 in Cc)
  3064.     header = header WrapField822("Cc",Cc)
  3065.     for (i = 1; i in Order; i++)
  3066.     if ((field = Order[i]) in Fields) {
  3067.         header = header field ": " Fields[field] "\n"
  3068.         delete Fields[field]
  3069.     }
  3070.     for (field in Fields) {
  3071.     header = header field ": " Fields[field] "\n"
  3072.     delete Fields[field]
  3073.     }
  3074.     return header
  3075. }
  3076. ### End of mail sending routines.
  3077. # WhoAmI 1.0 97/02/14
  3078. # 97/02/14 john h. dubois iii (john@armory.com)
  3079. # WhoAmI: return best attempt at determining what user owns this process.
  3080. # First, get USER from environment.  If that fails, try logname; it gives a
  3081. # better indication of who the user is than the uid does, since multiple login
  3082. # names may have the same uid.  But, check that the name returned by logname
  3083. # maps to the process' uid, as utmp may have bogus data or the user may have
  3084. # su'd.  If it doesn't, or logname fails, use the user name returned by id.
  3085. # For efficiency in multiple invokations, the user name is stored in
  3086. # _WhoAmI_user for reuse.
  3087. function WhoAmI(  Cmd,line,elem,logname,uiduser,uid,oFS) {
  3088.     if (_WhoAmI_user != "")
  3089.     return _WhoAmI_user 
  3090.     if ("USER" in ENVIRON && ENVIRON["USER"] != "")
  3091.     return _WhoAmI_user = ENVIRON["USER"]
  3092.     Cmd = "exec /usr/bin/logname 2>/dev/null"
  3093.     Cmd | getline logname
  3094.     close(Cmd)
  3095.     Cmd = "exec /usr/bin/id"
  3096.     Cmd | getline line
  3097.     close(Cmd)
  3098.     split(line,elem,"[()=]")
  3099.     uiduser = elem[3]
  3100.     if (logname == uiduser)
  3101.     return _WhoAmI_user = logname
  3102.     uid = elem[2]
  3103.     oFS = FS
  3104.     FS = ":"
  3105.     while ((getline < "/etc/passwd") == 1)
  3106.     if ($1 == logname) {
  3107.         if ($3 == uid)
  3108.         return _WhoAmI_user = logname
  3109.         break
  3110.     }
  3111.     return _WhoAmI_user = uiduser
  3112. }
  3113.