home *** CD-ROM | disk | FTP | other *** search
- #!/usr/local/bin/gawk -f
- # @(#) bills.gawk 3.2 97/07/25
- # 90/03/27 john h. dubois iii (john@armory.com)
- # 91/01/13 cleaned up the code, added features
- # 91/06/30 Changed field separator to be one or more tabs
- # 92/02/16 Added help option
- # 92/05/01 Converted to #!gawk script.
- # Use gawk because it pays attention when we change ARGV[],
- # and has strftime()
- # 94/04/23 Use .billrc
- # 95/03/21 Added rx options. Made insensitive to case of payment type.
- # Changed format of results.
- # 95/06/13 Added [Not] options. Warn about dates in the future, wrong # of
- # fields, and invalid payment values.
- # Print total only if more than one payment type found.
- # 95/06/18 Added gGHk options.
- # 95/08/03 Let extra info be stored in type field.
- # 95/08/17 Added sorting, pPReEand options, Purc & $/Purc fields.
- # 96/01/20 Search for rcfile in $UHOME as well as $HOME.
- # Make dollars part of values optional.
- # 96/01/21 Added AlMTw options.
- # 96/05/06 Search only for BILLS in the environment.
- # 96/05/27 Print whole line after error encountered. Added y option.
- # 96/06/21 Added i option.
- # 97/07/25 Let # at the start of 2nd or 3rd field (in addition to 1st) make a
- # line be treated as a comment, so sorting by date can work on it.
-
- BEGIN {
- Name = "bills"
- rcFile = ".billrc"
- Width = 19
- Usage = "Usage: " Name \
- " [-aAeGhHklnNMopPrRyY] [-g<tag>] [-T<tabstring>] [-t<type[,...]>]\n"\
- " "\
- " [-s<startdate>] [-d<enddate>] [-w<width>] [-E<mapfile>] [file ...]"
- # pseudo -f arg is for BILLS rcfile option
- ARGC = Opts(Name,Usage,"f:s:d:rot:NgGkneE:aRpPw<T:AhHi:lMxyY",0,
- "~/" rcFile ":$UHOME/" rcFile,
- "BILLS,STARTDATE,ENDDATE,RUNNING,ORDER,TYPES,NOHEADER,TAG,EMPTYTAG,NOCHECK,"\
- "NAMETYPES,EXPENSETYPES,EXPENSEFILE,AMOUNTSORT,REVERSESORT,NUMSORT,AVGSORT,"\
- "WIDTH,TABSTRING",1,"")
- if ("h" in Options) {
- printf \
- "%s: report total and per-day expenditures for various types of payments.\n"\
- "%s\n"\
- "If no files are given, the file given in the environment variable BILLS is\n"\
- "used. BILLS can also be set in the configuration file (see below) in this\n"\
- "form:\n"\
- "BILLS=filename\n"\
- "If no filenames are given and BILLS is not set, the standard input is used.\n"\
- "For each type of payment found, a line is printed giving statistics for\n"\
- "payments of that type: the total amount, the amount per day, week, month,\n"\
- "and year during the period from the first to last day in the input, and\n"\
- "the number of payments and the average value per payment.\n" \
- "The output is sorted by the payment type by default.\n"\
- "Payments may be grouped by one of three methods: payment-type, payee-name,\n"\
- "and expense-type. By default, the payment type is used; this is the type\n"\
- "given as the third field of each payment line. Alternately, the name of\n"\
- "the payee can be used as the payment type, with the name optionally mapped\n"\
- "through an expense-types file to a canonical name to allow for variations.\n"\
- "If the mapping file is used, names that are not found in it are preceded\n"\
- "by a '!'. Finally, in the expense-types file each name can be mapped to\n"\
- "an expense type, so that names can be categorized as e.g. grocery,\n"\
- "gasoline, etc. expenses. Names not found in the map are mapped to the\n"\
- "type \"misc\".\n"\
- "Options:\n"\
- "Some of the following options can also be set by assigning values to\n"\
- "variables in a configuration file named %s, which is searched for in the\n"\
- "invoking user's home directory and in the directory specified by the\n"\
- "environment variable UHOME, if it is set (if both files exist, values set\n"\
- "in the former take precedence). Variables are assigned to with the\n"\
- "syntax: varname=value or in the case of flags, by simply putting the\n"\
- "indicated variable name in the file without a value. Variable names are\n"\
- "given in parentheses in the option descriptions.\n",Name,Usage,rcFile
- printf TabOpts(\
- "General options:\n"\
- "-h: Print this help.\n"\
- "-H: Print a description of the data and expense-types file formats.\n"\
- "-r: Print each line as it is read, preceded by the new sum for the type\n"\
- " of payment the line is for. (RUNNING)\n"\
- "-E<filename>: Set the name of the expense-types mapping file. (MAPFILE)\n"\
- "Output sorting options (default: sort by payment type):\n"\
- "-a: Sort by amounts. (AMOUNTSORT)\n"\
- "-p: Sort by number of payments. (NUMSORT)\n"\
- "-P: Sort by average payment amount. (AVGSORT)\n"\
- "-R: Display the sorted output in reverse order. (REVERSESORT)\n"\
- "Record selection options (default: process all records):\n"\
- "-s<startdate>: Entries with dates before <startdate> will be ignored.\n"\
- " <startdate> should be in the form [yy/]mm/dd. (STARTDATE)\n"\
- "-d<enddate>: Entries with dates after <enddate> will be ignored.\n"\
- " <enddate> should be in the form [yy/]mm/dd. (ENDDATE)\n"\
- "-g<tag>: Process only lines that come after the tag line containing <tag>.\n"\
- " (TAG)\n"\
- "-G: Process only lines that come after the tag line containing an empty\n"\
- " tag. (EMPTYTAG)\n"\
- "-t<type[,type,...]>: Process only records having a payment type given in\n"\
- " the comma-separated list. Types are not case sensitive. (TYPES)\n"\
- "-i<payment-info-pattern>: Process only records having a payment-info field\n"\
- " (the part of the payment type field that comes after the colon, if\n"\
- " any) that matches <payment-info-pattern>, which may be a pattern in\n"\
- " the style of egrep(C), implicitely anchored at the start and end. If\n"\
- " a null string or other pattern that would match an empty string is\n"\
- " given with -i, in addition to matching records that have an empty\n"\
- " payment-info field (nothing after the colon) it will match records\n"\
- " having no payment-info field (no colon in the payment type field).\n"\
- "File checking options:\n"\
- "-o: Warn about entries that have dates that are out of sequence. (ORDER)\n"\
- "-y: Print (only) all types found in the mapfile.\n"\
- "-Y: Like -y, except that types are printed in multiple columns.\n"\
- "-l: Print (only) all names found, sorted by length.\n"\
- "-M: Print (only) names not found in the mapfile.\n"\
- "-A: Print (only) aliases found in the types file that are not mapped.\n"\
- "-k: Skip some of the normal file sanity checks, to process large files\n"\
- " slightly faster. (NOCHECK)\n"\
- "Grouping options (default: group by payment type):\n"\
- "-n: Use the payee name as the grouping type. If an expense-type mapping\n"\
- " file is given it will be used to canonicalize the names. Names not\n"\
- " found in the mapfile will be prefixed with '!'. (NAMETYPES)\n"\
- "-e: Use the expense type as the grouping type. If no expense-type\n"\
- " filename is given, the default filename "types" is used; it is\n"\
- " expected to exist in the same directory that the first datafile is in.\n"\
- " (EXPENSETYPES)\n"\
- "Output formatting options:\n"\
- "-N: Do not print any header or total lines. (NOHEADER)\n"\
- "-w<width>: Set the maximum width of the Type field in the output (the\n"\
- " default is %d characters). A width of 0 causes the type field to\n"\
- " never be truncated. (WIDTH)\n"\
- "-T<tabstring>: Turn off constant-width Type field formatting; instead, the\n"\
- " type field is separated from the next by <tabstring>. (TABSTRING)\n",
- " "," -"),Width
- ExitNow = 1
- }
- if ("H" in Options) {
- printf \
- "Data file format:\n"\
- "Input lines are comment lines, payment lines, and tag lines.\n"\
- "Lines that have a # at the start of the first, second, or third field are\n"\
- "comment lines and are ignored.\n" \
- "Payment lines have this format:\n" \
- "date<tabs>amount<tabs>type[:<pay-info>]<tabs>name[<tabs>comment]\n" \
- "where date is in the form [year/]month/day (month should be numeric),\n"\
- "amount is a decimal number without a leading '$',\n"\
- "type is the type of payment (e.g., Cash, Check, etc.),\n"\
- "name is the name of the party the payment was made to,\n"\
- "and comment is anything (it is not used, other than being printed if the\n"\
- "r option is given).\n"\
- "If a year is not given the payment is taken to have occured in the\n"\
- "current year. Payment types are not case sensitive. The first occurance\n"\
- "of each payment type sets the capitalization that will be used in the\n"\
- "report. A colon (:) and anything after it in the payment type are the\n"\
- "option pay-info field and are ignored by default. This allows extra\n"\
- "information to be stored in the type field; for example, the month in\n"\
- "which the transaction appeared on a credit card bill.\n"\
- "Fields are separated by one or more tabs.\n"\
- "Examples:\n"\
- "95/04/05 1.49 Cash Santa Cruz Hardware Sanding paper\n"\
- "95/07/23 16.04 MC:08 British Petroleum\n"\
- "Tag lines have this format:\n"\
- "date tag [tag-value]\n"\
- "where date is the same as for a payment line, tag is the literal string\n"\
- "\"tag\", and tag-value is either empty (to use with the G option) or a tag\n"\
- "string to be given with the g option. Tags are case sensitive.\n"\
- "Example:\n"\
- "95/05/16 tag StartNow\n"\
- "The input need not be sorted by date. The order will affect the results\n"\
- "only when the tag options are used.\n"\
- "\n"\
- "Expense-types mapping file format:\n"\
- "Input lines are comment lines, expense-type lines, and alias lines.\n"\
- "Lines that start with # are comment lines and are ignored.\n"\
- "Expense-type lines have this format:\n"\
- "Name<tabs>Type\n"\
- "where Name is a name that may occur in a datafile and Type is an\n"\
- "expense-type.\n"\
- "Alias lines have this format:\n"\
- "Name=Canonical-name\n"\
- "where Name is a name that may occur in a datafile, and Canonical-name is\n"\
- "a name that is mapped to an expense-type elsewhere in the file.\n"\
- "The = character can only occur on an alias line, separating the two names.\n"\
- "Examples:\n"\
- "Logo's books\n"\
- "Longs=Longs Drugs\n"\
- "Longs Drugs drugstore\n"
-
- ExitNow = 1
- }
- if (ExitNow)
- exit(0)
- if ((Err = ExclusiveOptions("gG",Options)) != "") {
- printf "Error: %s\n",Err
- Err = 1
- exit(1)
- }
- Debug = "x" in Options
-
- if (ARGC < 2 && "f" in Options) {
- ARGV[1] = Options["f"]
- ARGC = 2
- }
-
- FS = "\t+"
- if ("s" in Options)
- start = makedate(Options["s"])
- else
- start = 0
- if ("d" in Options)
- end = makedate(Options["d"])
- else
- end = 2000000000
- Running = "r" in Options
- Order = "o" in Options
- AmountSort = "a" in Options
- NumSort = "p" in Options
- AvgSort = "P" in Options
- ReverseSort = "R" in Options
- NoHeader = "N" in Options
- # Turn n option on if [AlMy] is given so that mapfile will be read.
- NameTypes = (unMappedOnly = "M" in Options) ||
- (typesOnly = ("y" in Options || "Y" in Options)) ||
- (namesOnly = "l" in Options) || (badAliasesOnly = "A" in Options)
- Fast = "k" in Options
- if ("T" in Options) {
- tabString = Options["T"]
- Width = 0
- }
- if ("g" in Options)
- Tag = Options["g"]
- if ("i" in Options)
- infoPat = "^(" Options["i"] ")$"
- TagWait = "g" in Options || "G" in Options
- if ("w" in Options)
- Width = Options["w"]
- if (TypesGiven = ("t" in Options))
- MakeSet(Types,tolower(Options["t"]),",")
- first = 10000000
- last = 0
- # "date +%y" | getline year
- year = strftime("%y")
- year *= 10000
- split("0 31 59 90 120 151 181 212 243 273 304 334",months," ")
- CurDate = strftime("%y%m%d")
- if ((NameTypes = NameTypes || "n" in Options) ||
- (ExpTypes = "e" in Options)) {
- # Read expense types file
- if ("E" in Options)
- ExpTypeFile = Options["E"]
- else if (ARGC > 1) {
- ExpTypeFile = ARGV[1]
- sub("[^/]*$","types",ExpTypeFile)
- }
- else # If reading stdin, expect tags in current dir
- ExpTypeFile = "types"
- if (Debug)
- printf "Reading mapfile %s\n",ExpTypeFile > "/dev/stderr"
- # If we cannot read the mapping file, and either it was given
- # explicitly or we have to have it for the e option, give up.
- if (ReadTypeFile(CanonMap,TypeMap,ExpTypeFile) == -1) {
- if ("E" in Options || ExpTypes) {
- printf \
- "Error reading expense-type mapping file %s:\n%s. Exiting.\n",
- ExpTypeFile,ERRNO
- Err = 1
- exit 1
- }
- if (Debug)
- printf \
- "Error reading expense-type mapping file %s:\n%s.\n",
- ExpTypeFile,ERRNO
- }
- else
- GoodMap = 1
- if (Debug)
- printf "Done reading mapfile.\n" > "/dev/stderr"
- if (typesOnly) {
- for (name in TypeMap)
- Types[TypeMap[name]]
- n = qsortByArbIndex(Types,k,0)
- if ("y" in Options)
- for (i = 1; i <= n; i++)
- print k[i]
- else {
- HeadTailInit(-1)
- PrintDown(k,2,COLUMNS-1)
- }
- }
- if (typesOnly || badAliasesOnly) {
- ExitNow = 1
- exit 0
- }
- }
- else
- TypeTypes = 1 # Collate by payment-type field
- }
-
- ### Start of record-processing blocks
-
- $1 ~ /^#/ || $2 ~ /^#/ || $3 ~ /^#/ {
- next
- }
-
- $2 == "tag" {
- if (TagWait) {
- if (!Fast)
- date = GetDate()
- if ($3 == Tag) {
- TagWait = 0
- if (Debug)
- printf "Found tag \"%s\" on input line %d\n",Tag,NR > \
- "/dev/stderr"
- }
- }
- next
- }
-
- {
- if (TagWait)
- next
- if (!Fast) {
- if (NF != 4 && NF != 5) {
- printf "Error: %d fields in record %d; should be 4 or 5\n>> %s\n",
- NF,NR,$0 > "/dev/stderr"
- next
- }
- # A $ value must be: optional minus sign, optional dollars part,
- # optional cents part, with at least one digit (so that just a minus
- # sign will not suffice).
- if ($2 !~ /^-?[0-9]*(\.[0-9]+)?$/ || $2 !~ /[0-9]/) {
- printf "Error: invalid payment value in record %d: %s\n>> %s\n",
- NR,$2,$0 > "/dev/stderr"
- next
- }
- }
- date = GetDate()
-
- if (TypeTypes) {
- Type = $3
- sub(":.*","",Type)
- }
- else {
- lName = tolower($4)
- if (NameTypes) {
- if (GoodMap) {
- if (lName in CanonMap)
- Type = CanonMap[lName]
- else {
- Type = CanonMap[lName] = $4
- unMapped[lName]
- }
- }
- else
- Type = $4
- }
- else # ExpTypes
- if (lName in CanonMap)
- Type = TypeMap[tolower(CanonMap[lName])]
- else
- Type = "misc"
- }
- LType = tolower(Type)
- if (infoPat != "") {
- payInfo = $3
- if (!sub("[^:]*:","",payInfo))
- payInfo = ""
- if (payInfo !~ infoPat)
- next
- }
-
- # Selection by date and type occurs here
- if (date >= start && date <= end && (!TypesGiven || LType in Types)) {
- if (date < first)
- first = date
- if (date > last)
- last = date
- # The first instance of each type sets its capitalization
- if (!(LType in Capitalized))
- Capitalized[LType] = Type
- sum[LType] += $2
- Count[LType]++
- if (Running) {
- if (Debug)
- printf "[%s] ",unmakedate(date)
- printf "%8.2f %s\n",sum[LType],$0
- }
- }
- }
-
- ### End of record-processing blocks
-
- function GetDate( date) {
- if ((date = makedate($1)) == -1) {
- printf "Bad date in record number %d: %s\n",NR,$1 > "/dev/stderr"
- next
- }
- if (date > CurDate)
- printf "Warning: Record %d has a date in the future: %s\n",
- NR,unmakedate(date) > "/dev/stderr"
- if (Order) {
- if (date < PrevDate)
- printf "Warning: Record %d has an earlier date than record %d.\n",
- NR,NR-1 > "/dev/stderr"
- # Set date to this date line even if it was out of order,
- # because the previous one might be the real culprit.
- PrevDate = date
- }
- return date
- }
-
- function Fmt(Amount) {
- return sprintf("%.2f",Amount)
- }
-
- function PrintLine(Type,Len, Amount,PerDay) {
- Amount = sum[Type]
- PerDay = Amount/datediff
- printf(Format,substr((Type in unMapped ? "!" : "")Capitalized[Type],1,Len),
- sprintf("%.1f",Amount/Total*100), Fmt(Amount),Fmt(PerDay),Fmt(PerDay * 7),
- Fmt(PerDay * 30),Fmt(PerDay * 365), Count[Type],
- Count[Type] ? Fmt(Amount/Count[Type]) : "-")
- return Amount
- }
-
- END {
- if (Err)
- exit Err
- if (ExitNow)
- exit(0)
- if (first == 10000000) {
- print "0 records processed." > "/dev/stderr"
- exit(0)
- }
- firstday = date2days(first)
- lastday = date2days(last)
- datediff = lastday - firstday
- # Start with name field with of 5 because that is how long "Total" is
- NameWidth = 5
- # Total for each type is in sum[type]
- for (Type in sum) {
- if ((l = length(Type)) > NameWidth)
- NameWidth = l
- if (Type != TotalType) {
- GotType = 1
- Total += sum[Type]
- TotalCount += Count[Type]
- if (Debug && !sum[Type])
- printf "0 sum for type %s\n",Type > "/dev/stderr"
- }
- }
- if (!GotType) {
- print "No types found?!" > "/dev/stderr"
- exit(1)
- }
- if (namesOnly || unMappedOnly) {
- if (namesOnly) {
- for (lName in Capitalized)
- nameLengths[lName] = length(lName)
- Num = qsortArbIndByValue(nameLengths,k)
- }
- else
- Num = qsortByArbIndex(unMapped,k)
- for (i = 1; i <= Num; i++)
- print Capitalized[k[i]]
- exit 0
- }
- if (!Total) {
- print "Sum of amounts is 0."
- exit(0)
- }
- FW = "8"
- if (Width)
- NameWidth = min(NameWidth,Width)
- if (tabString == "")
- Format = "%-" NameWidth "s %5s %" FW "s %" FW-2 "s %" FW-1 "s %" FW-1 \
- "s %" FW "s %4s %7s\n"
- else
- Format = "%s" tabString "%5s %" FW "s %" FW-2 "s %" FW-1 "s %" FW-1 \
- "s %" FW "s %4s %7s\n"
- if (!NoHeader) {
- printf("Period: %s to %s (%d days).\n",unmakedate(first),
- unmakedate(last),datediff)
- printf Format,
- "Type","%","$Total","$/Day","$/Week","$/Month","$/Year","Num","Avg$"
- }
- if (AmountSort)
- Num = qsortArbIndByValue(sum,k)
- else if (NumSort)
- Num = qsortArbIndByValue(Count,k)
- else if (AvgSort) {
- for (lName in sum)
- Ind[lName] = sum[lName] / Count[lName]
- Num = qsortArbIndByValue(Ind,k)
- }
- else
- Num = qsortByArbIndex(sum,k)
- if (ReverseSort)
- for (i = Num; i >= 1; i--)
- PrintLine(k[i],NameWidth)
- else
- for (i = 1; i <= Num; i++)
- PrintLine(k[i],NameWidth)
- if (NumElem(sum) > 1 && !NoHeader) {
- TotalType = "_total"
- Capitalized[TotalType] = "Total"
- sum[TotalType] = Total
- Count[TotalType] = TotalCount
- PrintLine(TotalType,NameWidth)
- }
- }
-
- # Read mapping file TypeFile.
- # CanonMap[] maps every name to a canonical name.
- # TypeMap[] maps every canonical name to an expense type.
- # Array indexes are moved to lower case; values in CanonMap are mixed case.
- function ReadTypeFile(CanonMap,TypeMap,TypeFile,
- LineNum,l,Line,i,ret,ErrCount) {
- FS = "\t+|="
- while ((ret = (getline < TypeFile)) == 1) {
- LineNum++
- if ($0 ~ /^#/) # comment
- continue
- if (NF != 2 || $1 ~ /^ *$/ || $2 ~ /^ *$/) {
- printf \
- "Error on line %d of type file %s:\nLine does not have two fields:\n%s\n",
- LineNum,TypeFile,$0 > "/dev/stderr"
- continue
- }
- l = tolower($1)
- if ($0 ~ /\t/) {
- TypeMap[l] = $2
- CanonMap[l] = $1
- }
- else
- CanonMap[l] = $2
- Line[l] = LineNum
- }
- for (i in CanonMap) {
- What = CanonMap[i]
- if (!((l = tolower(What)) in TypeMap)) {
- if (badAliasesOnly) {
- if (!(l in badAliases)) {
- print What
- badAliases[l]
- }
- }
- else
- printf \
- "Error on line %d of type file %s:\n"\
- "%s is aliased to %s, which is not mapped.\n",
- Line[i],TypeFile,i,What > "/dev/stderr"
- delete CanonMap[i]
- ErrCount++
- }
- }
- if (ret)
- return -1
- else
- return ErrCount
- }
-
- # convert month/day or year/month/day date to yymmdd date
- # uses global "year" var if year not given
- function makedate(InDate,Elements,d,date) {
- Elements = split(InDate,d,"/")
- date = d[1] * 100 + d[2]
- if (Elements == 2)
- date += year
- else if (Elements == 3)
- date = date * 100 + d[3]
- else
- return -1
- return date
- }
-
- # convert yymmdd date to yy/mm/dd date
- function unmakedate(Date) {
- return substr(Date,1,2) "/" substr(Date,3,2) "/" substr(Date,5,2)
- }
-
- function date2days(date) {
- return(date % 100 + months[0 + substr(date,3,2)] + substr(date,1,2) * 365)
- }
-
- # MakeSet: make a set from a list.
- # An index with the name of each element of the list
- # is created in the given array.
- # Input variables:
- # Elements is a string containing the list of elements.
- # Sep is the character that separates the elements of the list.
- # Output variables:
- # Set is the array.
- # Return value: the number of elements added to the set.
- function MakeSet(Set,Elements,Sep, i,Num,Names) {
- Num = split(Elements,Names,Sep)
- for (i = 1; i <= Num; i++)
- Set[Names[i]]
- return Num
- }
-
- # Returns the number of elements in set Set
- function NumElem(Set, elem,Num) {
- for (elem in Set)
- Num++
- return Num
- }
-
- ### Begin min,max,In routines
-
- function min(a,b) {
- if (a < b)
- return a
- else
- return b
- }
-
- function max(a,b) {
- if (a > b)
- return a
- else
- return b
- }
-
- function In(Val,Min,Max) {
- return (Min <= Val && Val <= Max)
- }
-
- # Return (in Ind) the indices of the elements with the smallest value in A.
- # The smallest value is returned as the function value.
- # If there are no elements in A, null is returned.
- function arrMin(A,Ind, i,min) {
- for (i in A)
- if (min == "" || A[i] < min) {
- DeleteAll(Ind)
- min = A[i]
- Ind[i]
- }
- else if (A[i] == min)
- Ind[i]
- return min
- }
-
- ### End min,max,In routines
- ### Begin qsort routines
-
- # Arr[] is an array of values with arbitrary indices.
- # k[] is returned with numeric indices 1..n.
- # The values in k[] are the indices of Arr[],
- # ordered so that if Arr[] is stepped through
- # in the order Arr[k[1]] .. Arr[k[n]], it will be stepped
- # through in order of the values of its elements.
- # The return value is the number of elements in the arrays (n).
- function qsortArbIndByValue(Arr,k, ArrInd,ElNum) {
- ElNum = 0
- for (ArrInd in Arr)
- k[++ElNum] = ArrInd
- qsortSegment(Arr,k,1,ElNum)
- return ElNum
- }
-
- # Sort a segment of an array.
- # Arr[] contains data with arbitrary indices.
- # k[] has indices 1..nelem, with the indices of arr[] as values.
- # This function sorts the elements of arr that are pointed to by
- # k[start..end], swapping the values of elements of k[] so that
- # when this function returns arr[k[start..end]] will be in order.
- function qsortSegment(Arr,k,start,end, left,right,sepval,tmp,tmpe,tmps) {
- # handle two-element case explicitly for a tiny speedup
- if ((end - start) == 1) {
- if (Arr[tmps = k[start]] > Arr[tmpe = k[end]]) {
- k[start] = tmpe
- k[end] = tmps
- }
- return
- }
- # Make sure comparisons act on these as numbers
- left = start+0
- right = end+0
- sepval = Arr[k[int((left + right) / 2)]]
- # Make every element <= sepval be to the left of every element > sepval
- while (left < right) {
- while (Arr[k[left]] < sepval)
- left++
- while (Arr[k[right]] > sepval)
- right--
- if (left < right) {
- tmp = k[left]
- k[left++] = k[right]
- k[right--] = tmp
- }
- }
- if (left == right)
- if (Arr[k[left]] < sepval)
- left++
- else
- right--
- if (start < right)
- qsortSegment(Arr,k,start,right)
- if (left < end)
- qsortSegment(Arr,k,left,end)
- }
-
- # Arr[] is an array of values with arbitrary indices.
- # k[] is returned with numeric indices 1..n.
- # The values in k are the indices of Arr[],
- # ordered so that if Arr[] is stepped through
- # in the order Arr[k[1]] .. Arr[k[n]], it will be stepped
- # through in order of the values of its indices.
- # The return value is the number of elements in the arrays (n).
- # If the indexes are numeric, Numeric should be true, so that they can be
- # compared as such rather than as strings. Numeric indexes do not have to be
- # contiguous.
- function qsortByArbIndex(Arr,k,Numeric, ArrInd,ElNum) {
- ElNum = 0
- if (Numeric)
- # Indexes do not preserve numeric type, so must be forced
- for (ArrInd in Arr)
- k[++ElNum] = ArrInd+0
- else
- for (ArrInd in Arr)
- k[++ElNum] = ArrInd
- qsortNumIndByValue(k,1,ElNum)
- return ElNum
- }
-
- # Arr is an array of elements with contiguous numeric indexes to be sorted
- # by value.
- # start and end are the starting and ending indexes of the range to be sorted.
- function qsortNumIndByValue(Arr,start,end, left,right,sepval,tmp,tmpe,tmps) {
- # handle two-element case explicitly for a tiny speedup
- if ((start - end) == 1) {
- if ((tmps = Arr[start]) > (tmpe = Arr[end])) {
- Arr[start] = tmpe
- Arr[end] = tmps
- }
- return
- }
- left = start+0
- right = end+0
- sepval = Arr[int((left + right) / 2)]
- while (left < right) {
- while (Arr[left] < sepval)
- left++
- while (Arr[right] > sepval)
- right--
- if (left <= right) {
- tmp = Arr[left]
- Arr[left++] = Arr[right]
- Arr[right--] = tmp
- }
- }
- if (start < right)
- qsortNumIndByValue(Arr,start,right)
- if (left < end)
- qsortNumIndByValue(Arr,left,end)
- }
-
- ### End qsort routines
-
- ### Start of ProcArgs library
- # @(#) ProcArgs 1.11 96/12/08
- # 92/02/29 john h. dubois iii (john@armory.com)
- # 93/07/18 Added "#" arg type
- # 93/09/26 Do not count -h against MinArgs
- # 94/01/01 Stop scanning at first non-option arg. Added ">" option type.
- # Removed meaning of "+" or "-" by itself.
- # 94/03/08 Added & option and *()< option types.
- # 94/04/02 Added NoRCopt to Opts()
- # 94/06/11 Mark numeric variables as such.
- # 94/07/08 Opts(): Do not require any args if h option is given.
- # 95/01/22 Record options given more than once. Record option num in argv.
- # 95/06/08 Added ExclusiveOptions().
- # 96/01/20 Let rcfiles be a colon-separated list of filenames.
- # Expand $VARNAME at the start of its filenames.
- # Let varname=0 and -option- turn off an option.
- # 96/05/05 Changed meaning of 7th arg to Opts; now can specify exactly how many
- # of the vars should be searched for in the environment.
- # Check for duplicate rcfiles.
- # 96/05/13 Return more specific error values. Note: ProcArgs() and InitOpts()
- # now return various negatives values on error, not just -1, and
- # Opts() may set Err to various positive values, not just 1.
- # Added AllowUnrecOpt.
- # 96/05/23 Check type given for & option
- # 96/06/15 Re-port to awk
- # 96/10/01 Moved file-reading code into ReadConfFile(), so that it can be
- # used by other functions.
- # 96/10/15 Added OptChars
- # 96/11/01 Added exOpts arg to Opts()
- # 96/11/16 Added ; type
- # 96/12/08 Added Opt2Set() & Opt2Sets()
- # 96/12/27 Added CmdLineOpt()
-
- # optlist is a string which contains all of the possible command line options.
- # A character followed by certain characters indicates that the option takes
- # an argument, with type as follows:
- # : String argument
- # ; Non-empty string argument
- # * Floating point argument
- # ( Non-negative floating point argument
- # ) Positive floating point argument
- # # Integer argument
- # < Non-negative integer argument
- # > Positive integer argument
- # The only difference the type of argument makes is in the runtime argument
- # error checking that is done.
-
- # The & option is a special case used to get numeric options without the
- # user having to give an option character. It is shorthand for [-+.0-9].
- # If & is included in optlist and an option string that begins with one of
- # these characters is seen, the value given to "&" will include the first
- # char of the option. & must be followed by a type character other than ":"
- # or ";".
- # Note that if e.g. &> is given, an option of -.5 will produce an error.
-
- # Strings in argv[] which begin with "-" or "+" are taken to be
- # strings of options, except that a string which consists solely of "-"
- # or "+" is taken to be a non-option string; like other non-option strings,
- # it stops the scanning of argv and is left in argv[].
- # An argument of "--" or "++" also stops the scanning of argv[] but is removed.
- # If an option takes an argument, the argument may either immediately
- # follow it or be given separately.
- # "-" and "+" options are treated the same. "+" is allowed because most awks
- # take any -options to be arguments to themselves. gawk 2.15 was enhanced to
- # stop scanning when it encounters an unrecognized option, though until 2.15.5
- # this feature had a flaw that caused problems in some cases. See the OptChars
- # parameter to explicitly set the option-specifier characters.
-
- # If an option that does not take an argument is given,
- # an index with its name is created in Options and its value is set to the
- # number of times it occurs in argv[].
-
- # If an option that does take an argument is given, an index with its name is
- # created in Options and its value is set to the value of the argument given
- # for it, and Options[option-name,"count"] is (initially) set to the 1.
- # If an option that takes an argument is given more than once,
- # Options[option-name,"count"] is incremented, and the value is assigned to
- # the index (option-name,instance) where instance is 2 for the second occurance
- # of the option, etc.
- # In other words, the first time an option with a value is encountered, the
- # value is assigned to an index consisting only of its name; for any further
- # occurances of the option, the value index has an extra (count) dimension.
-
- # The sequence number for each option found in argv[] is stored in
- # Options[option-name,"num",instance], where instance is 1 for the first
- # occurance of the option, etc. The sequence number starts at 1 and is
- # incremented for each option, both those that have a value and those that
- # do not. Options set from a config file have a value of 0 assigned to this.
-
- # Options and their arguments are deleted from argv.
- # Note that this means that there may be gaps left in the indices of argv[].
- # If compress is nonzero, argv[] is packed by moving its elements so that
- # they have contiguous integer indices starting with 0.
- # Option processing will stop with the first unrecognized option, just as
- # though -- was given except that unlike -- the unrecognized option will not be
- # removed from ARGV[]. Normally, an error value is returned in this case.
- # If AllowUnrecOpt is true, it is not an error for an unrecognized option to
- # be found, so the number of remaining arguments is returned instead.
- # If OptChars is not a null string, it is the set of characters that indicate
- # that an argument is an option string if the string begins with one of the
- # characters. A string consisting solely of two of the same option-indicator
- # characters stops the scanning of argv[]. The default is "-+".
- # argv[0] is not examined.
- # The number of arguments left in argc is returned.
- # If an error occurs, the global string OptErr is set to an error message
- # and a negative value is returned.
- # Current error values:
- # -1: option that required an argument did not get it.
- # -2: argument of incorrect type supplied for an option.
- # -3: unrecognized (invalid) option.
- function ProcArgs(argc,argv,OptList,Options,compress,AllowUnrecOpt,OptChars,
- ArgNum,ArgsLeft,Arg,ArgLen,ArgInd,Option,Pos,NumOpt,Value,HadValue,specGiven,
- NeedNextOpt,GotValue,OptionNum,Escape,dest,src,count,c,OptTerm,OptCharSet)
- {
- # ArgNum is the index of the argument being processed.
- # ArgsLeft is the number of arguments left in argv.
- # Arg is the argument being processed.
- # ArgLen is the length of the argument being processed.
- # ArgInd is the position of the character in Arg being processed.
- # Option is the character in Arg being processed.
- # Pos is the position in OptList of the option being processed.
- # NumOpt is true if a numeric option may be given.
- ArgsLeft = argc
- NumOpt = index(OptList,"&")
- OptionNum = 0
- if (OptChars == "")
- OptChars = "-+"
- while (OptChars != "") {
- c = substr(OptChars,1,1)
- OptChars = substr(OptChars,2)
- OptCharSet[c]
- OptTerm[c c]
- }
- for (ArgNum = 1; ArgNum < argc; ArgNum++) {
- Arg = argv[ArgNum]
- if (length(Arg) < 2 || !((specGiven = substr(Arg,1,1)) in OptCharSet))
- break # Not an option; quit
- if (Arg in OptTerm) {
- delete argv[ArgNum]
- ArgsLeft--
- break
- }
- ArgLen = length(Arg)
- for (ArgInd = 2; ArgInd <= ArgLen; ArgInd++) {
- Option = substr(Arg,ArgInd,1)
- if (NumOpt && Option ~ /[-+.0-9]/) {
- # If this option is a numeric option, make its flag be & and
- # its option string flag position be the position of & in
- # the option string.
- Option = "&"
- Pos = NumOpt
- # Prefix Arg with a char so that ArgInd will point to the
- # first char of the numeric option.
- Arg = "&" Arg
- ArgLen++
- }
- # Find position of flag in option string, to get its type (if any).
- # Disallow & as literal flag.
- else if (!(Pos = index(OptList,Option)) || Option == "&") {
- if (AllowUnrecOpt) {
- Escape = 1
- break
- }
- else {
- OptErr = "Invalid option: " specGiven Option
- return -3
- }
- }
-
- # Find what the value of the option will be if it takes one.
- # NeedNextOpt is true if the option specifier is the last char of
- # this arg, which means that if the option requires a value it is
- # the next arg.
- if (NeedNextOpt = (ArgInd >= ArgLen)) { # Value is the next arg
- if (GotValue = ArgNum + 1 < argc)
- Value = argv[ArgNum+1]
- }
- else { # Value is included with option
- Value = substr(Arg,ArgInd + 1)
- GotValue = 1
- }
-
- if (HadValue = AssignVal(Option,Value,Options,
- substr(OptList,Pos + 1,1),GotValue,"",++OptionNum,!NeedNextOpt,
- specGiven)) {
- if (HadValue < 0) # error occured
- return HadValue
- if (HadValue == 2)
- ArgInd++ # Account for the single-char value we used.
- else {
- if (NeedNextOpt) { # option took next arg as value
- delete argv[++ArgNum]
- ArgsLeft--
- }
- break # This option has been used up
- }
- }
- }
- if (Escape)
- break
- # Do not delete arg until after processing of it, so that if it is not
- # recognized it can be left in ARGV[].
- delete argv[ArgNum]
- ArgsLeft--
- }
- if (compress != 0) {
- dest = 1
- src = argc - ArgsLeft + 1
- for (count = ArgsLeft - 1; count; count--) {
- ARGV[dest] = ARGV[src]
- dest++
- src++
- }
- }
- return ArgsLeft
- }
-
- # Assignment to values in Options[] occurs only in this function.
- # Option: Option specifier character.
- # Value: Value to be assigned to option, if it takes a value.
- # Options[]: Options array to return values in.
- # ArgType: Argument type specifier character.
- # GotValue: Whether any value is available to be assigned to this option.
- # Name: Name of option being processed.
- # OptionNum: Number of this option (starting with 1) if set in argv[],
- # or 0 if it was given in a config file or in the environment.
- # SingleOpt: true if the value (if any) that is available for this option was
- # given as part of the same command line arg as the option. Used only for
- # options from the command line.
- # specGiven is the option specifier character use, if any (e.g. - or +),
- # for use in error messages.
- # Global variables: OptErr
- # Return value: negative value on error, 0 if option did not require an
- # argument, 1 if it did & used the whole arg, 2 if it required just one char of
- # the arg.
- # Current error values:
- # -1: Option that required an argument did not get it.
- # -2: Value of incorrect type supplied for option.
- # -3: Bad type given for option &
- function AssignVal(Option,Value,Options,ArgType,GotValue,Name,OptionNum,
- SingleOpt,specGiven, UsedValue,Err,NumTypes) {
- # If option takes a value... [
- NumTypes = "*()#<>]"
- if (Option == "&" && ArgType !~ "[" NumTypes) { # ]
- OptErr = "Bad type given for & option"
- return -3
- }
-
- if (UsedValue = (ArgType ~ "[:;" NumTypes)) { # ]
- if (!GotValue) {
- if (Name != "")
- OptErr = "Variable requires a value -- " Name
- else
- OptErr = "option requires an argument -- " Option
- return -1
- }
- if ((Err = CheckType(ArgType,Value,Option,Name,specGiven)) != "") {
- OptErr = Err
- return -2
- }
- # Mark this as a numeric variable; will be propogated to Options[] val.
- if (ArgType != ":" && ArgType != ";")
- Value += 0
- if ((Instance = ++Options[Option,"count"]) > 1)
- Options[Option,Instance] = Value
- else
- Options[Option] = Value
- }
- # If this is an environ or rcfile assignment & it was given a value...
- else if (!OptionNum && Value != "") {
- UsedValue = 1
- # If the value is "0" or "-" and this is the first instance of it,
- # do not set Options[Option]; this allows an assignment in an rcfile to
- # turn off an option (for the simple "Option in Options" test) in such
- # a way that it cannot be turned on in a later file.
- if (!(Option in Options) && (Value == "0" || Value == "-"))
- Instance = 1
- else
- Instance = ++Options[Option]
- # Save the value even though this is a flag
- Options[Option,Instance] = Value
- }
- # If this is a command line flag and has a - following it in the same arg,
- # it is being turned off.
- else if (OptionNum && SingleOpt && substr(Value,1,1) == "-") {
- UsedValue = 2
- if (Option in Options)
- Instance = ++Options[Option]
- else
- Instance = 1
- Options[Option,Instance]
- }
- # If this is a flag assignment without a value, increment the count for the
- # flag unless it was turned off. The indicator for a flag being turned off
- # is that the flag index has not been set in Options[] but it has an
- # instance count.
- else if (Option in Options || !((Option,1) in Options))
- # Increment number of times this flag seen; will inc null value to 1
- Instance = ++Options[Option]
- Options[Option,"num",Instance] = OptionNum
- return UsedValue
- }
-
- # Option is the option letter
- # Value is the value being assigned
- # Name is the var name of the option, if any
- # ArgType is one of:
- # : String argument
- # ; Non-null string argument
- # * Floating point argument
- # ( Non-negative floating point argument
- # ) Positive floating point argument
- # # Integer argument
- # < Non-negative integer argument
- # > Positive integer argument
- # specGiven is the option specifier character use, if any (e.g. - or +),
- # for use in error messages.
- # Returns null on success, err string on error
- function CheckType(ArgType,Value,Option,Name,specGiven, Err,ErrStr) {
- if (ArgType == ":")
- return ""
- if (ArgType == ";") {
- if (Value == "")
- Err = "must be a non-empty string"
- }
- # A number begins with optional + or -, and is followed by a string of
- # digits or a decimal with digits before it, after it, or both
- else if (Value !~ /^[-+]?([0-9]+|[0-9]*\.[0-9]+|[0-9]+\.)$/)
- Err = "must be a number"
- else if (ArgType ~ "[#<>]" && Value ~ /\./)
- Err = "may not include a fraction"
- else if (ArgType ~ "[()<>]" && Value < 0)
- Err = "may not be negative"
- # (
- else if (ArgType ~ "[)>]" && Value == 0)
- Err = "must be a positive number"
- if (Err != "") {
- ErrStr = "Bad value \"" Value "\". Value assigned to "
- if (Name != "")
- return ErrStr "variable " substr(Name,1,1) " " Err
- else {
- if (Option == "&")
- Option = Value
- return ErrStr "option " specGiven substr(Option,1,1) " " Err
- }
- }
- else
- return ""
- }
-
- # Note: only the above functions are needed by ProcArgs.
- # The rest of these functions call ProcArgs() and also do other
- # option-processing stuff.
-
- # Opts: Process command line arguments.
- # Opts processes command line arguments using ProcArgs()
- # and checks for errors. If an error occurs, a message is printed
- # and the program is exited.
- #
- # Input variables:
- # Name is the name of the program, for error messages.
- # Usage is a usage message, for error messages.
- # OptList the option description string, as used by ProcArgs().
- # MinArgs is the minimum number of non-option arguments that this
- # program should have, non including ARGV[0] and +h.
- # If the program does not require any non-option arguments,
- # MinArgs should be omitted or given as 0.
- # rcFiles, if given, is a colon-seprated list of filenames to read for
- # variable initialization. If a filename begins with ~/, the ~ is replaced
- # by the value of the environment variable HOME. If a filename begins with
- # $, the part from the character after the $ up until (but not including)
- # the first character not in [a-zA-Z0-9_] will be searched for in the
- # environment; if found its value will be substituted, if not the filename will
- # be discarded.
- # rcfiles are read in the order given.
- # Values given in them will not override values given on the command line,
- # and values given in later files will not override those set in earlier
- # files, because AssignVal() will store each with a different instance index.
- # The first instance of each variable, either on the command line or in an
- # rcfile, will be stored with no instance index, and this is the value
- # normally used by programs that call this function.
- # VarNames is a comma-separated list of variable names to map to options,
- # in the same order as the options are given in OptList.
- # If EnvSearch is given and nonzero, the first EnvSearch variables will also be
- # searched for in the environment. If set to -1, all values will be searched
- # for in the environment. Values given in the environment will override
- # those given in the rcfiles but not those given on the command line.
- # NoRCopt, if given, is an additional letter option that if given on the
- # command line prevents the rcfiles from being read.
- # See ProcArgs() for a description of AllowUnRecOpt and optChars, and
- # ExclusiveOptions() for a description of exOpts.
- # Special options:
- # If x is made an option and is given, some debugging info is output.
- # h is assumed to be the help option.
-
- # Global variables:
- # The command line arguments are taken from ARGV[].
- # The arguments that are option specifiers and values are removed from
- # ARGV[], leaving only ARGV[0] and the non-option arguments.
- # The number of elements in ARGV[] should be in ARGC.
- # After processing, ARGC is set to the number of elements left in ARGV[].
- # The option values are put in Options[].
- # On error, Err is set to a positive integer value so it can be checked for in
- # an END block.
- # Return value: The number of elements left in ARGV is returned.
- # Must keep OptErr global since it may be set by InitOpts().
- function Opts(Name,Usage,OptList,MinArgs,rcFiles,VarNames,EnvSearch,NoRCopt,
- AllowUnrecOpt,optChars,exOpts, ArgsLeft,e) {
- if (MinArgs == "")
- MinArgs = 0
- ArgsLeft = ProcArgs(ARGC,ARGV,OptList NoRCopt,Options,1,AllowUnrecOpt,
- optChars)
- if (ArgsLeft < (MinArgs+1) && !("h" in Options)) {
- if (ArgsLeft >= 0) {
- OptErr = "Not enough arguments"
- Err = 4
- }
- else
- Err = -ArgsLeft
- printf "%s: %s.\nUse -h for help.\n%s\n",
- Name,OptErr,Usage > "/dev/stderr"
- exit 1
- }
- if (rcFiles != "" && (NoRCopt == "" || !(NoRCopt in Options)) &&
- (e = InitOpts(rcFiles,Options,OptList,VarNames,EnvSearch)) < 0)
- {
- print Name ": " OptErr ".\nUse -h for help." > "/dev/stderr"
- Err = -e
- exit 1
- }
- if ((exOpts != "") && ((OptErr = ExclusiveOptions(exOpts,Options)) != ""))
- {
- printf "%s: Error: %s\n",Name,OptErr > "/dev/stderr"
- Err = 1
- exit 1
- }
- return ArgsLeft
- }
-
- # ReadConfFile(): Read a file containing var/value assignments, in the form
- # <variable-name><assignment-char><value>.
- # Whitespace (spaces and tabs) around a variable (leading whitespace on the
- # line and whitespace between the variable name and the assignment character)
- # is stripped. Lines that do not contain an assignment operator or which
- # contain a null variable name are ignored, other than possibly being noted in
- # the return value. If more than one assignment is made to a variable, the
- # first assignment is used.
- # Input variables:
- # File is the file to read.
- # Comment is the line-comment character. If it is found as the first non-
- # whitespace character on a line, the line is ignored.
- # Assign is the assignment string. The first instance of Assign on a line
- # separates the variable name from its value.
- # If StripWhite is true, whitespace around the value (whitespace between the
- # assignment char and trailing whitespace on the line) is stripped.
- # VarPat is a pattern that variable names must match.
- # Example: "^[a-zA-Z][a-zA-Z0-9]+$"
- # If FlagsOK is true, variables are allowed to be "set" by being put alone on
- # a line; no assignment operator is needed. These variables are set in
- # the output array with a null value. Lines containing nothing but
- # whitespace are still ignored.
- # Output variables:
- # Values[] contains the assignments, with the indexes being the variable names
- # and the values being the assigned values.
- # Lines[] contains the line number that each variable occured on. A flag set
- # is record by giving it an index in Lines[] but not in Values[].
- # Return value:
- # If any errors occur, a string consisting of descriptions of the errors
- # separated by newlines is returned. In no case will the string start with a
- # numeric value. If no errors occur, the number of lines read is returned.
- function ReadConfigFile(Values,Lines,File,Comment,Assign,StripWhite,VarPat,
- FlagsOK,
- Line,Status,Errs,AssignLen,LineNum,Var,Val) {
- if (Comment != "")
- Comment = "^" Comment
- AssignLen = length(Assign)
- if (VarPat == "")
- VarPat = "." # null varname not allowed
- while ((Status = (getline Line < File)) == 1) {
- LineNum++
- sub("^[ \t]+","",Line)
- if (Line == "") # blank line
- continue
- if (Comment != "" && Line ~ Comment)
- continue
- if (Pos = index(Line,Assign)) {
- Var = substr(Line,1,Pos-1)
- Val = substr(Line,Pos+AssignLen)
- if (StripWhite) {
- sub("^[ \t]+","",Val)
- sub("[ \t]+$","",Val)
- }
- }
- else {
- Var = Line # If no value, var is entire line
- Val = ""
- }
- if (!FlagsOK && Val == "") {
- Errs = Errs \
- sprintf("\nBad assignment on line %d of file %s: %s",
- LineNum,File,Line)
- continue
- }
- sub("[ \t]+$","",Var)
- if (Var !~ VarPat) {
- Errs = Errs sprintf("\nBad variable name on line %d of file %s: %s",
- LineNum,File,Var)
- continue
- }
- if (!(Var in Lines)) {
- Lines[Var] = LineNum
- if (Pos)
- Values[Var] = Val
- }
- }
- if (Status)
- Errs = Errs "\nCould not read file " File
- close(File)
- return Errs == "" ? LineNum : substr(Errs,2) # Skip first newline
- }
-
- # Variables:
- # Data is stored in Options[].
- # rcFiles, OptList, VarNames, and EnvSearch are as as described for Opts().
- # Global vars:
- # Sets OptErr. Uses ENVIRON[].
- # If anything is read from any of the rcfiles, sets READ_RCFILE to 1.
- function InitOpts(rcFiles,Options,OptList,VarNames,EnvSearch,
- Line,Var,Pos,Vars,Map,CharOpt,NumVars,TypesInd,Types,Type,Ret,i,rcFile,
- fNames,numrcFiles,filesRead,Err,Values,retStr) {
- split("",filesRead,"") # make awk know this is an array
- NumVars = split(VarNames,Vars,",")
- TypesInd = Ret = 0
- if (EnvSearch == -1)
- EnvSearch = NumVars
- for (i = 1; i <= NumVars; i++) {
- Var = Vars[i]
- CharOpt = substr(OptList,++TypesInd,1)
- if (CharOpt ~ "^[:;*()#<>&]$")
- CharOpt = substr(OptList,++TypesInd,1)
- Map[Var] = CharOpt
- Types[Var] = Type = substr(OptList,TypesInd+1,1)
- # Do not overwrite entries from environment
- if (i <= EnvSearch && Var in ENVIRON &&
- (Err = AssignVal(CharOpt,ENVIRON[Var],Options,Type,1,Var,0)) < 0)
- return Err
- }
-
- numrcFiles = split(rcFiles,fNames,":")
- for (i = 1; i <= numrcFiles; i++) {
- rcFile = fNames[i]
- if (rcFile ~ "^~/")
- rcFile = ENVIRON["HOME"] substr(rcFile,2)
- else if (rcFile ~ /^\$/) {
- rcFile = substr(rcFile,2)
- match(rcFile,"^[a-zA-Z0-9_]*")
- envvar = substr(rcFile,1,RLENGTH)
- if (envvar in ENVIRON)
- rcFile = ENVIRON[envvar] substr(rcFile,RLENGTH+1)
- else
- continue
- }
- if (rcFile in filesRead)
- continue
- # rcfiles are liable to be given more than once, e.g. UHOME and HOME
- # may be the same
- filesRead[rcFile]
- if ("x" in Options)
- printf "Reading configuration file %s\n",rcFile > "/dev/stderr"
- retStr = ReadConfigFile(Values,Lines,rcFile,"#","=",0,"",1)
- if (retStr > 0)
- READ_RCFILE = 1
- else if (ret != "") {
- OptErr = retStr
- Ret = -1
- }
- for (Var in Lines)
- if (Var in Map) {
- if ((Err = AssignVal(Map[Var],
- Var in Values ? Values[Var] : "",Options,Types[Var],
- Var in Values,Var,0)) < 0)
- return Err
- }
- else {
- OptErr = sprintf(\
- "Unknown var \"%s\" assigned to on line %d\nof file %s",Var,
- Lines[Var],rcFile)
- Ret = -1
- }
- }
-
- if ("x" in Options)
- for (Var in Map)
- if (Map[Var] in Options)
- printf "(%s) %s=%s\n",Map[Var],Var,Options[Map[Var]] > \
- "/dev/stderr"
- else
- printf "(%s) %s not set\n",Map[Var],Var > "/dev/stderr"
- return Ret
- }
-
- # OptSets is a semicolon-separated list of sets of option sets.
- # Within a list of option sets, the option sets are separated by commas. For
- # each set of sets, if any option in one of the sets is in Options[] AND any
- # option in one of the other sets is in Options[], an error string is returned.
- # If no conflicts are found, nothing is returned.
- # Example: if OptSets = "ab,def,g;i,j", an error will be returned due to
- # the exclusions presented by the first set of sets (ab,def,g) if:
- # (a or b is in Options[]) AND (d, e, or f is in Options[]) OR
- # (a or b is in Options[]) AND (g is in Options) OR
- # (d, e, or f is in Options[]) AND (g is in Options)
- # An error will be returned due to the exclusions presented by the second set
- # of sets (i,j) if: (i is in Options[]) AND (j is in Options[]).
- # todo: make options given on command line unset options given in config file
- # todo: that they conflict with.
- function ExclusiveOptions(OptSets,Options,
- Sets,SetSet,NumSets,Pos1,Pos2,Len,s1,s2,c1,c2,ErrStr,L1,L2,SetSets,NumSetSets,
- SetNum,OSetNum) {
- NumSetSets = split(OptSets,SetSets,";")
- # For each set of sets...
- for (SetSet = 1; SetSet <= NumSetSets; SetSet++) {
- # NumSets is the number of sets in this set of sets.
- NumSets = split(SetSets[SetSet],Sets,",")
- # For each set in a set of sets except the last...
- for (SetNum = 1; SetNum < NumSets; SetNum++) {
- s1 = Sets[SetNum]
- L1 = length(s1)
- for (Pos1 = 1; Pos1 <= L1; Pos1++)
- # If any of the options in this set was given, check whether
- # any of the options in the other sets was given. Only check
- # later sets since earlier sets will have already been checked
- # against this set.
- if ((c1 = substr(s1,Pos1,1)) in Options)
- for (OSetNum = SetNum+1; OSetNum <= NumSets; OSetNum++) {
- s2 = Sets[OSetNum]
- L2 = length(s2)
- for (Pos2 = 1; Pos2 <= L2; Pos2++)
- if ((c2 = substr(s2,Pos2,1)) in Options)
- ErrStr = ErrStr "\n"\
- sprintf("Cannot give both %s and %s options.",
- c1,c2)
- }
- }
- }
- if (ErrStr != "")
- return substr(ErrStr,2)
- return ""
- }
-
- # The value of each instance of option Opt that occurs in Options[] is made an
- # index of Set[].
- # The return value is the number of instances of Opt in Options.
- function Opt2Set(Options,Opt,Set, count) {
- if (!(Opt in Options))
- return 0
- Set[Options[Opt]]
- count = Options[Opt,"count"]
- for (; count > 1; count--)
- Set[Options[Opt,count]]
- return count
- }
-
- # The value of each instance of option Opt that occurs in Options[] that
- # begins with "!" is made an index of nSet[] (with the ! stripped from it).
- # Other values are made indexes of Set[].
- # The return value is the number of instances of Opt in Options.
- function Opt2Sets(Options,Opt,Set,nSet, count,aSet,ret) {
- ret = Opt2Set(Options,Opt,aSet)
- for (value in aSet)
- if (substr(value,1,1) == "!")
- nSet[substr(value,2)]
- else
- Set[value]
- return ret
- }
-
- # Returns true if option Opt was given on the command line.
- function CmdLineOpt(Options,Opt, i) {
- for (i = 1; (Opt,"num",i) in Options; i++)
- if (Options[Opt,"num",i] != 0)
- return 1
- return 0
- }
- ### End of ProcArgs library
-
- # In multiline string OptDesc, insert string Tab at the start of every line
- # that begins with any of the characters in TabChars.
- # If the first character in Tab also occurs in TabChars, make sure it is the
- # last character in TabChars to avoid double indenting.
- function TabOpts(OptDesc,Tab,TabChars, i,len,c) {
- len = length(TabChars)
- for (i = 1; i <= len; i++) {
- c = substr(TabChars,i,1)
- sub("^" c,Tab c,OptDesc)
- gsub("\n" c,"\n" Tab c,OptDesc)
- }
- return OptDesc
- }
-
- ### Begin head-tail routines
-
- # @(#) HeadTail.awk 96/05/09
- # 95/04/28 Added tail routines.
- # 96/05/09 Added all args to HeadTailInit()
-
- # Turn on screen-bounded printing.
- # Current implementation sets global vars LINES, COLUMNS, LINEGAP, and COLGAP.
- # Sets the number of screen lines and rows to Lines and Rows.
- # If -1 is passed for either, turns off bounding in that dimension.
- # If either is not set or 0 is passed for it, its value is taken from the
- # environment, or if not set there, from terminfo, or if not set there, from
- # the defaults (24 and 80).
- # By default, the other functions in this library leave a "grace space" of
- # 1 column and 1 line. If LineGap or ColGap is passed and is a non-negative
- # value, the line gap is set to it.
- function HeadTailInit(Lines,Cols,LineGap,ColGap, Cmd) {
- # tput will use values in environment, but we want to avoid running
- # it if possible.
- if (Cols > 0)
- COLUMNS = Cols
- else if (!Cols)
- if ("COLUMNS" in ENVIRON)
- COLUMNS = ENVIRON["COLUMNS"]
- else {
- Cmd = "exec tput cols"
- Cmd | getline COLUMNS
- close(Cmd)
- if (COLUMNS == "")
- COLUMNS = 80
- }
- if (Lines > 0)
- LINES = Lines
- else if (!Lines)
- if ("LINES" in ENVIRON)
- LINES = ENVIRON["LINES"]
- else {
- Cmd = "exec tput lines"
- Cmd | getline LINES
- close(Cmd)
- if (LINES == "")
- LINES = 24
- }
- LINEGAP = (LineGap != "" && LineGap >= 0) ? LineGap : 1
- COLGAP = (ColGap != "" && ColGap >= 0) ? ColGap : 1
- }
-
- # Do screen-bound printing.
- # If LINES is >0, the last LINES-LINEGAP lines are kept in a circular buffer.
- # When TailFlush() is called, they are printed.
- # If LINES = 0, all lines are printed immediately.
- # If COLUMNS is >0, truncates Line to COLUMNS-COLGAP characters before printing
- # it.
- # Global vars: uses LINES & COLUMNS; sets/uses TailPtr;
- # saves lines in TailLines[] from 1..LINES-LINEGAP
- # Embedded newlines split the line into multiple lines; trailing newlines are
- # stripped. Tabs are expanded to spaces.
- function TailPrint(Line) {
- if (!LINES)
- print Line
- else {
- if (++TailPtr > (LINES-LINEGAP))
- TailPtr = 1
- TailLines[TailPtr] = Line
- }
- }
-
- function TailFlush( NumPrinted,Lines,Line,i,Buffer,PrintLines) {
- if (!LINES)
- return
- NumPrinted = 0
- PrintLines = LINES-LINEGAP
- # Since lines may contain multiple lines, we must create a buffer to be
- # printed by reading line buffer backwards.
- # Stop when we have copied enough lines, or if we wrap around to the end
- # and find that the entire line buffer was not used.
- while (NumPrinted < PrintLines && TailPtr in TailLines) {
- # Split line into individual lines, then process them last to first
- Num = split(TailLines[TailPtr],Lines,"\n")
- for (i = Num; i >= 1; i--) {
- Line = Lines[i]
- if (i == Num && Line == "") # discard trailing newline
- continue
- # Put this line at the front of the print buffer
- if (COLUMNS)
- Buffer = substr(TabEx(Line),1,COLUMNS - COLGAP) "\n" Buffer
- else
- Buffer = Line "\n" Buffer
- if (++NumPrinted == PrintLines)
- break
- }
- if (!--TailPtr) # Wrap pointer if neccessary
- TailPtr = PrintLines
- }
- printf "%s",Buffer
- }
-
- # Do screen-bound printing.
- # If LINES >0, returns 0 when LINES-LINEGAP lines have been printed by
- # HeadPrint(). Otherwise returns 1.
- # If COLUMNS is >0, truncates Line to COLUMNS-COLGAP characters before printing
- # it.
- # Global vars: uses LINES, COLUMNS, LINEGAP, COLGAP; sets/uses LinesPrinted.
- # Line should not include newlines.
- function HeadPrint(Line) {
- # Check first, in case some calls of this function to not check return
- # value, and in case LINES is 1.
- if (LINES && LinesPrinted >= (LINES-LINEGAP))
- return 0
- if (COLUMNS)
- print substr(Line,1,COLUMNS - COLGAP)
- else
- print Line
- if (LINES && ++LinesPrinted >= (LINES-LINEGAP))
- return 0
- return 1
- }
-
- function ColPrint(Line) {
- if (COLUMNS)
- print substr(Line,1,COLUMNS - COLGAP)
- else
- print Line
- return 1
- }
-
- ### End head-tail routines
-
- # Print the elements in Data[1..n] in multiple columns across the screen,
- # printing from the top to the bottom of the first column, then top to bottom
- # of second column, etc.
- # Gutter is the number of spaces to print between columns.
- # Width is the screen width to use.
- # As many columns as will fit are printed across.
- function PrintDown(Data,Gutter,Width, l,Cols,Rows,len,i,Ind,Row) {
- # Find max string length.
- l = 0
- for (i = 1; i in Data; i++)
- if ((len = length(Data[i])) > l)
- l = len
- i--
- if (!l)
- return
- l += Gutter
- Cols = int((Width+Gutter)/l)
- if (!Cols)
- Cols = 1
- Rows = int(i/Cols+Cols-1)
- for (Row = 1; Row <= Rows; Row++) {
- Ind = Row
- for (Col = 1; Col <= Cols; Col++) {
- if (Ind in Data)
- printf("%-*s",Col == Cols ? 0 : l,Data[Ind])
- Ind += Rows
- }
- print ""
- }
- }
-