home *** CD-ROM | disk | FTP | other *** search
- #!/usr/local/bin/gawk -f
- #!/u/johnd/bin/gawk -f
- # @(#) mbill.gawk 3.1 96/05/06
- # 92/09/06 john h. dubois iii (john@armory.com)
- # 92/12/09 Added help.
- # 93/03/04 Added p option
- # 93/06/19 Set Err when exiting due to invalid option
- # 94/03/11 Cleaned up
- # 94/07/05 2.0 Generalized from rent processing program.
- # 95/04/01 Modified to pay attention to the day of the month given for rate
- # change lines, to allow prorating.
- # 95/06/18 Print the sum of the owed values. Added c option.
- # 95/11/02 Let party names be given in assorted case
- # 95/12/16 3.0 Added ability to specify multiple types of bills, and B option.
- # 95/12/18 Added le options
- # 96/01/12 Do not print 0 amounts for months.
- # 96/05/06 Search only for MBILL in environment.
- #
- # todo: let verbose output be broken down by rate type.
- # todo: Let certain vars be set in bill file.
- # todo: Generalize who amounts are owed by & owed to & how they are specified
- # todo: Add option to only produce output for certain users.
- # todo: Let decimal fractions be given in payments. Currently gives bogus err.
- # Bugs:
- # When a future rate change occurs, it causes billing to be calculated up
- # through that date. Billing should end at the current (or specified) date
- # regardless of future changes.
-
- BEGIN {
- Run()
- exit 0
- }
-
- # Globals: see Setup().
- function Run( AllParties,Paid,LastPay,LastAmt,RateName,RateNames,TotalBilled) {
- Setup()
- # Make gawk know these are all arrays
- # Every party found is made an index of AllParties; it also maps the
- # lower-case version of each party name to a mixed-case version
- split("",AllParties)
- ProcFiles(ARGC,ARGV,Running,AllParties,Paid,LastPay,LastAmt,TotalBilled,
- RateNames,MRates,MRateChanges,PrintChanges,UnnamedName,Debug,
- BillLatest,BillThrough)
- if (Err != "")
- exit Err
- # AllParties[], Paid[], LastPay[], LastAmt[], TotalBilled[],
- # RateNames[], MRates[], and MRateChanges[] come from ProcFiles().
- # Brief, NoBreakdown, LastPayment, and Debug come from Setup().
- PrintResults(AllParties,Paid,LastPay,LastAmt,Brief,NoBreakdown,
- TotalBilled,RateNames,MRates,MRateChanges,LastPayment,Debug)
- }
-
- # Sets globals:
- # PrintChanges: Rate changes should be printed.
- # LastPayment: Last payments should be printed.
- # Debug: Debugging is on.
- # UnnamedName: The name to use for the unnamed bill type.
- # Running: Print lines as they are processed.
- # Brief: Print only a brief summary.
- # NoBreakdown: Do not print a breakdown of amounts owed.
- # BillLatest: Bill through last day of last month given in a file date.
- # BillThrough: A specific date to bill through (in epoch days).
- function Setup( Name,Usage,FileVar,hrcFile,rcFile,DefFile) {
- Name = "mbill"
- UnnamedName = "Bills"
- Usage = "Usage: " Name " [-bBchHlnpr] [-e<date>] [-u<name>] [bill-file ...]"
- FileVar = "MBILL"
- hrcFile = ".mbillrc" # Name without the leading ~/, for help
- rcFile = "~/" hrcFile
- # f is pseudo-arg for filename
- ARGC = Opts(Name,Usage,"f:bBlpru:ce:hHx",0,rcFile,
- FileVar ",BRIEF,NOBREAKDOWN,LASTDATE,LASTPAY,RUNNING,UNNAMED",1,"n")
- if (ARGC < 0) {
- print OptErr
- exit 1
- }
- if ((Err = ExclusiveOptions("l,e",Options)) != "") {
- printf "Error: %s\n",Err > "/dev/stderr"
- Err = 1
- exit(1)
- }
- DefFile = ".mbills"
- if ("h" in Options) {
- printf \
- "%s: track monthly bill payments.\n"\
- "%s\n"\
- "%s processes a billing file and uses the information in it to determine\n"\
- "the current amount owed by each party. If no bill-file is named, the\n"\
- "default file \"%s\" in the user's home directory is used. If the\n"\
- "environment variable %s is set, its value is used instead.\n"\
- "The bill-file describes regular monthly expenses and amounts paid to\n"\
- "satisfy those expenses. After processing, the amount owed by each party\n"\
- "is printed, along with a breakdown of how much is owed for each month. If\n"\
- "a party has a net amount owed (the sum of the amounts paid by that party\n"\
- "are less than the amounts owed), the amount paid is counted against the\n"\
- "oldest bills first; that is, the breakdown of the amount owed will show\n"\
- "everything from the end of the period billed for back through the point\n"\
- "which has been paid for.\n"\
- "If a file named %s exists in the user's home directory, some options\n"\
- "can be set in it. A variable name given for an option may be placed in\n"\
- "the file (one per line) to turn that option on. If the option takes a\n"\
- "value, it can be assigned to the variable in %s using the form:\n"\
- "variable=value\n"\
- "The %s variable can also be assigned a value in %s.\n"\
- "Options: \n"\
- "-b: Print only a brief summary, without headers or trailers, suitable for\n"\
- " input to another utility. Variable: BRIEF\n"\
- "-B: Do not print a breakdown of what is owed for each month.\n"\
- " Variable: NOBREAKDOWN\n"\
- "-h: Print this help.\n"\
- "-H: Print a description of the format of a bill file.\n"\
- "-l: Bill up through the last day of the month given on the latest rates or\n"\
- " payment line in the input files. The default is to bill up through\n"\
- " the last day of the current month. Variable: LASTDATE.\n"\
- "-e<date>: Bill up through <date>. <date> should be given in the form\n"\
- " yy/mm or yy/mm/dd. If no day of month is given, billing is done up\n"\
- " through the last day of the given month. All payments are applied\n"\
- " even if a date that precedes some of them is given.\n"\
- "-n: Do not read %s file. Programs that process the output of %s -b\n"\
- " can use this to make sure unwanted options are not turned on.\n"\
- "-c: Print rate changes.\n"\
- "-r: Print lines from bill files as they are processed. Variable: RUNNING\n"\
- "-p: Also print the date of last payment for each party. Variable: LASTPAY\n"\
- "-u<name>: Set the output name for the unnamed rate type to be <name>. The\n"\
- " default is \"%s\". Variable: UNNAMED\n",
- Name,Usage,Name,DefFile,FileVar,hrcFile,hrcFile,FileVar,hrcFile,
- hrcFile,Name,UnnamedName
- exit(0)
- }
- if ("H" in Options) {
- print \
- " Lines in a billing file take one of the following two forms:\n"\
- "\n"\
- "date [rate-type] party-name=amount ...\n"\
- "date <party-name> amount-paid comment\n"\
- "\n"\
- " Fields are separated by whitespace. Dates are given as yy/mm/dd.\n"\
- "Blank lines and lines that begin with '#' are considered comment lines and\n"\
- "are ignored. Case is ignored; the capitalization that is given in the\n"\
- "first occurance of each bill and party name is used for output purposes.\n"\
- " A line which has only one field after the date or which has fields of\n"\
- "the form party-name=value is a rates line. These lines set the rates for\n"\
- "a particular rate type, starting at the given date. If there was already\n"\
- "a rate of the given type in effect, the old rate ends on the day before\n"\
- "the new rate begins. If a rate-type is not given (the first field after\n"\
- "the date has an '=' in it), the line sets the rates for the \"unnamed\"\n"\
- "rate type. Using the unnamed rate type can be convenient if there is only\n"\
- "one rate type. \n"\
- " The party-name=amount fields set the monthly rates for the given\n"\
- "rate-type. All rates previously in effect for the rate-type are replaced.\n"\
- "Parties who had rates for the given rate-type when its rates were last set\n"\
- "and who are not mentioned in the new rates have their rates set to 0 (they\n"\
- "are removed from the rate-type). If there are only two fields on a rates\n"\
- "line (there are no party-name=amount fields), the line sets all the rates\n"\
- "for the given type to 0 (all parties are removed from it). This can be\n"\
- "used to end billing for a rate type, or to specify a period during which\n"\
- "no billing for the rate-type should take place. The special name 'Rates'\n"\
- "refers to the unnamed rate; that can be used to set its rates to 0.\n"\
- " Lines other than comment lines and rates lines are payment lines. A\n"\
- "payment line must have at least two fields after the date. Payment lines\n"\
- "are used to record payments that should be applied toward the bills. \n"\
- "Unless the -l or -p option is given, the dates on these lines are not used\n"\
- "other than in a check for sequentiality; the amount-paid is simply added\n"\
- "to the total for the named party for comparison to the total amount owed. \n"\
- "The total amount owed is calculated from the rates lines and extends\n"\
- "through the current month, unless -l or -e is given. The comment field is\n"\
- "not required and is not used.\n"\
- "Example:\n"\
- "92/06/01 Rent Alpha=345 Beta=335 Gamma=345 Delta=270\n"\
- "92/06/02 Delta 370 100 Jun, 270 Jul (also paid 100 deposit, 270 last)\n"\
- "92/06/03 Beta 120 toward May\n"\
- "92/06/09 Alpha 50 Balance of May\n"\
- "92/06/21 Gamma 690 Jun, Jul"
- exit 0
- }
- if (ARGC < 2) {
- if ("f" in Options)
- ARGV[1] = Options["f"]
- else
- ARGV[1] = ENVIRON["HOME"] "/" DefFile
- ARGC = 2
- }
- if (PrintChanges = ("c" in Options))
- print "Date Total Amounts"
- LastPayment = "p" in Options
- Debug = "x" in Options
- if ("u" in Options)
- UnnamedName = Options["u"]
- BillLatest = "l" in Options
- Running = "r" in Options
- Brief = "b" in Options
- NoBreakdown = "B" in Options
- if ("e" in Options) {
- if ((BillThrough = date2day(Options["e"],Fields)) == -1) {
- printf \
- "Bad date \"%s\" given with -e; should be in the form yy/mm/dd\n" \
- > "/dev/stderr"
- Err = 1
- exit 1
- }
- if (!(3 in Fields))
- BillThrough = endOfMonth(BillThrough)
- }
- }
-
- # Open each file; pass each line to appropriate processing routine.
- # Sets globals FILENAME and FNR for ErrExit()
- function ProcFiles(ARGC,ARGV,PrintRunning,AllParties,Paid,LastPay,LastAmt,
- TotalBilled,RateNames,MRates,MRateChanges,PrintChanges,UnnamedName,Debug,
- BillLatest,BillThrough,
- ArgInd,Day,LastDay,ret,RateInd,RateName,Rates,FinalDate) {
- for (ArgInd = 1; ArgInd < ARGC; ArgInd++) {
- FILENAME = ARGV[ArgInd]
- FNR = 0
- while ((ret = (getline < FILENAME)) == 1) {
- FNR++
- if (!NF)
- continue
- if (PrintRunning == 1)
- print $0
- if ($1 ~ /^#/)
- continue
- if ((Day = date2day($1)) == -1)
- ErrExit("Bad date.")
- if (Day < LastDay)
- ErrExit("Line out of date sequence.")
- LastDay = Day
- # A rates line is one that:
- # has an = in the 2nd or 3rd field (unnamed or named rate),
- # or which has only 2 fields (setting a named rate to 0).
- if (index($2,"=") || NF == 2 || index($3,"=")) {
- if (BillThrough && Day > BillThrough) {
- if (Debug)
- printf "Ignoring rate line with date %s\n",
- $1 > "/dev/stderr"
- continue
- }
- RateInd = 2
- # If the first rate field doesn't assign a rate value,
- # it is the name of this rate
- if ($RateInd !~ "=") {
- RateName = ($RateInd == "Rates") ? UnnamedName : $RateInd
- RateInd++
- }
- else
- RateName = UnnamedName
- # Allow 2nd field to be Rates for backward compatibility
- ChangeRates(Day,TotalBilled,MRates,MRateChanges,RateName,
- AllParties,Rates,RateNames,RateInd,Debug,PrintChanges)
- }
- else
- PaymentLine(Day,AllParties,Paid,LastPay,LastAmt)
- }
- if (ret)
- ErrExit("Error reading file: " ERRNO)
- }
- # Bill at the final rate for the period up to the last day of
- # the last month seen or the current month, or up to a specific date.
- if (BillLatest)
- FinalDate = endOfMonth(LastDay)+1
- else if (BillThrough)
- FinalDate = BillThrough+1
- else
- FinalDate = -1
- for (RateName in RateNames)
- ChangeRates(FinalDate,TotalBilled,MRates,MRateChanges,
- RateName,AllParties,Rates,RateNames,0,Debug)
- }
-
- # Accumulate amounts paid.
- # Uses globals $*
- function PaymentLine(Day,AllParties,Paid,LastPay,LastAmt, Party) {
- Party = tolower($2)
- if (!(Party in AllParties))
- ErrExit("Unknown party \"" $2 "\".")
- if ($3 !~ "^[0-9]+$")
- ErrExit("Bad rate \"" Amount "\".")
- Paid[Party] += $3
- # Entries are checked for date monotonicity elsewhere
- LastPay[Party] = Day
- LastAmt[Party] = $3
- }
-
- # OwedForMonths generates a string describing how much is owed for each month,
- # starting with the current month and continuing back in time until the
- # entire amount owed is accounted for.
- # Party is the party to find amounts owed by.
- # Month is the month to start at (the final month that is being billed for).
- # Amount is the amount owed.
- # RateFind[RateType,Party,Month] tells how much is owed for each month.
- # RateNames[] is the set of rate names used as part of the index of RateFind[];
- # the value is the mixed-case version of the rate name for printing.
- # FirstMonth is the first month that was billed for, for safety.
- function OwedForMonths(Party,Month,Amount,RateFind,RateNames,FirstMonth,
- Num2Month,
- S,ForThisMonth,Rate,RateName) {
- for (; Amount > 0 && Month >= FirstMonth; Month--) {
- ForThisMonth = 0
- for (RateName in RateNames)
- if ((RateName,Month) in RateFind)
- ForThisMonth += min(Amount,
- Rate = int(MRates[RateName,RateFind[RateName,Month],Party]+0.5))
- else {
- if (Debug)
- print "No rate for %s for month %s\n",RateName,Month \
- > "/dev/stderr"
- continue
- }
- if (ForThisMonth > 0)
- S = S sprintf("; %s: $%d",Num2Month[Month % 12 + 1],ForThisMonth)
- if (Amount < Rate)
- S = S sprintf(" of $%d",Rate)
- Amount -= ForThisMonth
- }
- if (Month < FirstMonth) {
- printf "Error in OwedForMonths():\n"\
- "Left with balance of %d while generating string for %s\n",Amount,Party
- Err = 1
- exit 1
- }
- return substr(S,3)
- }
-
- # Generate RateFind[] and FinalParties[]
- function MakeRateFind(RateNames,MRates,RateFind,Debug,AllParties,FinalParties,
- RateName,LastRateChange,Month,LastMonth,FirstMonth,Party) {
- FirstMonth = day2month(MRates["FirstDay"])
- # MRates["LastDay"] is the day of the last rate change, which will be the
- # pseudo-ratechange that is used to finish billing for the last real rate
- # change. It is the day after the real rates ended.
- # So, subtract 1 from it to get the final day of actual billing.
- LastMonth = day2month(MRates["LastDay"] - 1)
- for (RateName in RateNames) { # Build RateFind[]
- LastRateChange = ""
- for (Month = FirstMonth; Month <= LastMonth; Month++) {
- if ((RateName,Month) in MRateChanges) {
- LastRateChange = Month
- if (Debug) {
- printf "Prorated rates for %s for %s:",RateNames[RateName],
- month2date(Month) > "/dev/stderr"
- for (Party in AllParties)
- if ((RateName,Month,Party) in MRates)
- printf " %s=%s",AllParties[Party],
- MRates[RateName,Month,Party] > "/dev/stderr"
- print "" > "/dev/stderr"
- }
- }
- # Make RateFind[ratetype,month] map the month to the index in
- # Rates[] that gives the rate that was in effect that month.
- if (LastRateChange != "") {
- RateFind[RateName,Month] = LastRateChange
- if (Debug)
- printf "%s rate for %s was set %s\n",RateNames[RateName],
- month2date(Month),month2date(LastRateChange) > "/dev/stderr"
- }
- }
- }
- if (Debug)
- print "" > "/dev/stderr"
-
- # Find all parties who are party to at least one of the rates at the time
- # billing ended.
- # Use RateFind[] to find the last rate setting for each rate type.
- for (RateName in RateNames)
- for (Party in AllParties)
- if ((RateName,LastMonth) in RateFind &&
- (RateName,RateFind[RateName,LastMonth],Party) in MRates)
- FinalParties[Party]
- if (Debug) {
- printf "Final parties:" > "/dev/stderr"
- for (Party in FinalParties)
- printf " %s",AllParties[Party] > "/dev/stderr"
- printf "\n" > "/dev/stderr"
- }
- }
-
- function PrintResults(AllParties,Paid,LastPay,LastAmt,Brief,NoBreakdown,
- TotalBilled,RateNames,MRates,MRateChanges,LastPayment,Debug,
- RateFind,Party,Owed,Format,CWidth,OwedWidth,Date,LastMonth,LastDay,t,Tot,
- FirstDay,RateName,FinalParties,FirstMonth,LastRateChange,Month,TotOwed,
- Num2Month) {
-
- FirstMonth = day2month(FirstDay = MRates["FirstDay"])
- # Since the last day is always set to the date of a rate change,
- # and a psuedo-ratechange is done for the first day of the next month after
- # the current month, MRates["LastDay"] will hold that day.
- # Subtract one from it to get the last day of the last month that was
- # billed for.
- LastMonth = day2month(LastDay = MRates["LastDay"] - 1)
- if (Debug)
- print "" > "/dev/stderr"
- MakeRateFind(RateNames,MRates,RateFind,Debug,AllParties,FinalParties)
-
-
- split("Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec",Num2Month,",")
- OwedWidth = 4 # Width for amount-owed field
- if (Brief)
- for (Party in AllParties) {
- Tot = int(TotalBilled[Party] + 0.5)
- Owed = Tot - Paid[Party]
- if (Owed < 1 && !(Party in FinalParties))
- delete AllParties[Party]
- else {
- printf "%-8s %" OwedWidth "d",AllParties[Party],Tot-Paid[Party]
- if (NoBreakdown)
- print ""
- else
- printf " %s\n",
- OwedForMonths(Party,LastMonth,Owed,RateFind,RateNames,
- FirstMonth,Num2Month)
- }
- }
- else {
- printf "Period: %s to %s. Rate types:",day2date(FirstDay),
- day2date(LastDay)
- for (RateName in RateNames)
- printf " %s",RateNames[RateName]
- print ""
- CWidth = 5 # digits for cumulative values (total & paid)
- Format = "%-8s %" CWidth "s %" CWidth "s %" OwedWidth "s"
- printf Format,"Party","Total","Paid","Owed"
- if (NoBreakdown)
- print ""
- else
- print " Breakdown"
- for (Party in AllParties) {
- Tot = int(TotalBilled[Party] + 0.5)
- Owed = Tot - Paid[Party]
- if (Debug)
- printf "%s owes %.2f\n",AllParties[Party],Owed > "/dev/stderr"
- if (Owed < 1 && !(Party in FinalParties))
- delete AllParties[Party]
- else {
- printf Format,AllParties[Party],Tot,Paid[Party]+0,Owed
- if (NoBreakdown)
- print ""
- else
- print " " \
- OwedForMonths(Party,LastMonth,Owed,RateFind,RateNames,
- FirstMonth,Num2Month)
- TotOwed += Owed
- }
- }
- printf "Total owed: %.2f\n",TotOwed
- }
- if (LastPayment) {
- printf "\nLast payments:\n"
- for (Party in AllParties)
- printf "%-10s %8s %5s\n",AllParties[Party],
- Party in LastPay ? day2date(LastPay[Party]) : "NEVER",
- Party in LastAmt ? "$" LastAmt[Party] : ""
- }
- }
-
- ## Library routines
-
- ### Start of mbill library
-
- ## Start of rate change processing routines
- # ChangeRates: process a rate-change line.
- # RateName is the rate type.
- # Add the amounts owed from the last rate change to this rate change to the
- # balances (TotalBilled[party]) for all of the parties who were included in
- # the last rate change.
- # Record the new rates in Rates[ratename,party] so that the same can be done
- # when the next rate change occurs.
- # Record the rates and the month they took effect in
- # MRates[ratename,month,party] so that if there is an unpaid balance the
- # amounts owed for each month can be displayed.
- # Set MRateChanges[RateName,Month] so that the data in MRates[] can be found.
- # Make each party an index of AllParties[], so that error checking can be
- # done on payment lines.
- # Set MRates["FirstDay"] to the day of the first Rates line processed and
- # MRates["LastDay"] to the day for this rate change for anything that
- # wants to iterate over all months.
- # Set MRates[ratename,"FirstDay"] to the day of the first Rates line found for
- # this rate type, and MRates[ratename,"LastDay"] to the day for this rate
- # change for anything that needs to know the first & last month for each
- # rate type.
- # Makes every rate name encountered an index (in lower case) of RateNames[],
- # with the value being the name in mixed case as first encountered.
- # After the last rate change, ChangeRates should be called once more for each
- # rate type to process up to the current day, with RateInd set to 0 to
- # indicate that no new amounts should be added.
- # The last parameter does not need to be passed in this case.
- # If -1 is passed for Day, billing will be done for the period from the last
- # rate change up to the last day of the current month.
- #
- # Globals: $*
- #
- # Uses month2day(), month2days(), month2date(), day2date(), ErrExit(), Bill()
- # Day is the unix-day of the rate change. $RateInd..$NF are the rates.
- #
- function ChangeRates(Day,TotalBilled,MRates,MRateChanges,RateName,
- AllParties,Rates,RateNames,RateInd,Debug,PrintChanges,
- DateF,Month,i,Party,Amount,Total,t,CurMonth,MixedName,MixedParty) {
- RateName = tolower(MixedName = RateName)
- if (Day == -1) {
- # Get time beforehand so that all strftime()s will get the same time
- t = systime()
- CurMonth = (strftime("%Y",t)-1970)*12+strftime("%m",t)-1
- Day = month2day(CurMonth) + monthdays(CurMonth)
- if (Debug) {
- printf "\nSetting last month of billing period for %s to: %s\n",
- RateNames[RateName],month2date(CurMonth) > "/dev/stderr"
- printf "Setting last day of billing period for %s to: %s\n",
- RateNames[RateName],day2date(Day-1) > "/dev/stderr"
- }
- }
- if (!("FirstDay" in MRates)) {
- MRates["FirstDay"] = Day
- if (Debug)
- printf "First day: %s\n",day2date(Day) > "/dev/stderr"
- }
- # If this is the first line for this rate
- if (!((RateName,"FirstDay") in MRates)) {
- RateNames[RateName] = MixedName
- MRates[RateName,"FirstDay"] = Day
- if (Debug)
- printf "First day for %s: %s\n",RateNames[RateName],day2date(Day) \
- > "/dev/stderr"
- }
- else
- Bill(RateName,RateNames,MRates[RateName,"LastDay"],Day,Rates,
- TotalBilled,MRates,MRateChanges,AllParties,Debug)
- MRates["LastDay"] = Day
- MRates[RateName,"LastDay"] = Day
- # If doing final billing, no more to do
- if (!RateInd)
- return
- for (Party in AllParties)
- delete Rates[RateName,Party]
- for (i = RateInd; i <= NF; i++) {
- MixedParty = Amount = $i
- sub("=[^=]*","",MixedParty)
- sub(".*=","",Amount)
- if (MixedParty !~ "^[a-zA-Z]+$")
- ErrExit("Bad party name \"" MixedParty "\" on Rates line.")
- if (Amount !~ "^[0-9]+")
- ErrExit("Bad rate \"" Amount "\" on Rates line.")
- Party = tolower(MixedParty)
- Total += Rates[RateName,Party] = Amount
- if (!(Party in AllParties)) {
- AllParties[Party] = MixedParty
- if (Debug)
- printf "New party: %s\n",MixedParty > "/dev/stderr"
- }
- }
- if (Debug)
- print "Rate line for next period: " $0 > "/dev/stderr"
- if (PrintChanges) {
- $2 = sprintf("%5d",Total)
- print $0
- }
- }
-
- # RateName: The rate type being billed for.
- # StartDay: The day the rates went into effect.
- # EndDay: the day the rate change occured (billing is done up through the
- # previous day).
- # Rates[ratename,party]: the rates in effect for the period to be billed for.
- # TotalBilled[party] is incremented for the last billing period for each party.
- # Creates the index MRateChanges[RateName,Month].
- # MRates[ratename,month,party] is set to the monthly rate for each party.
- # Uses day2YMD(), day2date(), month2date(), SBill()
- function Bill(RateName,RateNames,StartDay,EndDay,Rates,TotalBilled,MRates,
- MRateChanges,AllParties,Debug,
- Month,StartMonth,EndMonth,StartDate,EndDate,StartDOM,EndDOM,
- StartNDays,EndNDays) {
- day2YMD(StartDay,StartDate)
- day2YMD(--EndDay,EndDate)
- StartMonth = day2month(StartDay)
- EndMonth = day2month(EndDay)
- StartDOM = StartDate["d"]
- EndDOM = EndDate["d"]
- StartNDays = monthdays(StartMonth)
- EndNDays = monthdays(EndMonth)
- if (Debug)
- printf "\nPeriod: %s-%s "\
- "Days in 1st mo: %d Days in last mo: %d\n", day2date(StartDay),
- day2date(EndDay), StartNDays,EndNDays > "/dev/stderr"
- if (StartMonth == EndMonth) {
- if (Debug)
- printf \
- "Start and end month are the same (%s); billing for %d days, %f mo.\n",
- month2date(StartMonth),(EndDOM-StartDOM+1),
- (EndDOM-StartDOM+1)/StartNDays > "/dev/stderr"
- SBill(RateName,RateNames,Rates,(EndDOM-StartDOM+1)/StartNDays,
- TotalBilled,StartMonth,MRates,MRateChanges,AllParties,Debug)
- return
- }
- if (StartDOM != 1) {
- if (Debug)
- printf \
- "Billing for fractional first month %s (%d days, %f month).\n",
- month2date(StartMonth),(StartNDays-StartDOM+1),
- (StartNDays-StartDOM+1)/StartNDays > "/dev/stderr"
- SBill(RateName,RateNames,Rates,(StartNDays-StartDOM+1)/StartNDays,
- TotalBilled,StartMonth,MRates,MRateChanges,AllParties,Debug)
- StartMonth++ # Have dealt with start month now
- }
- if (EndDOM != EndNDays) {
- if (Debug)
- printf \
- "Billing for fractional last month %s (%d days, %f month).\n",
- month2date(EndMonth),(EndNDays-EndDOM+1),
- (StartNDays-StartDOM+1)/StartNDays > "/dev/stderr"
- SBill(RateName,RateNames,Rates,EndDOM/EndNDays,TotalBilled,EndMonth,
- MRates,MRateChanges,AllParties,Debug)
- EndMonth--
- }
- if ((EndMonth - StartMonth) >= 0) {
- if (Debug)
- printf "Billing for %d full month(s) (%s-%s)\n",
- EndMonth-StartMonth+1,month2date(StartMonth),month2date(EndMonth) \
- > "/dev/stderr"
- SBill(RateName,RateNames,Rates,EndMonth-StartMonth+1,TotalBilled,
- StartMonth,MRates,MRateChanges,AllParties,Debug)
- }
- }
-
- # RateName: The rate type being billed for.
- # Rates[ratename,party]: the rates in effect for the period to be billed for.
- # Months: The number of months (may be non-integer) that the rates were in
- # effect.
- # The amount each party owes for this period is added to TotalBilled[party].
- # Makes (RateName,StartMonth) an index of MRateChanges[].
- # If a fractional month is being processed, the amount billed is added to
- # MRates[ratename,month,party]. If full months are being processed,
- # MRates[ratename,month,party] is set to the monthly rate.
- function SBill(RateName,RateNames,Rates,Months,TotalBilled,StartMonth,MRates,
- MRateChanges,AllParties,Debug, Party,Amount) {
- MRateChanges[RateName,StartMonth]
- for (Party in AllParties) {
- if ((RateName,Party) in Rates) {
- TotalBilled[Party] += Amount = Rates[RateName,Party] * Months
- if (Months < 1)
- MRates[RateName,StartMonth,Party] += Amount
- else
- MRates[RateName,StartMonth,Party] = Rates[RateName,Party]
- if (Debug) {
- if (!(RateName in RateNames))
- printf "%s not found in RateNames[]?!\n",
- RateName > "/dev/stderr"
- else
- printf \
- "Billing %s $%.2f for %.2g mo. of %s @ %.2f/mo.; total = $%.2f\n",
- AllParties[Party],Amount,Months,RateNames[RateName],
- Rates[RateName,Party],TotalBilled[Party] > "/dev/stderr"
- }
- }
- else if (Debug)
- printf "%s is not a party to rate %s\n",AllParties[Party],
- RateNames[RateName] > "/dev/stderr"
- }
- }
- ## End of rate change processing routines
- ### End of mbill library
-
- ### Begin date-days routines
- # @(#) date-days 1.2 95/12/18
- # 95/12/18 Added endOfMonth()
- # Fixed date2day() to work correctly with no month-day specified.
-
- # YMD2day(year,month,day-of-month) returns the number of days that passed from
- # 1970 Jan 1 to the given date.
- # All parameters should be given in numeric form.
- # If year < 70, it is assumed to be part of the 2000 century
- # If year in (70..99), 1900.
- # Globals: sets and uses MDays[]
- function YMD2day(Year,Month,Day, LeapDays) {
- Year+=0
- Month+=0
- if (Year < 70)
- Year += 100
- else if (Year >= 100)
- Year -= 1900
- # Year is now the number of years since 1900.
- LeapDays = int((Year - 68) / 4)
- if (Month <= 2 && Year % 4 == 0)
- LeapDays -= 1
- if (!(0 in MDays))
- split("0 31 59 90 120 151 181 212 243 273 304 334 365",MDays," ")
- return (Year - 70) * 365 + MDays[Month] + Day - 1 + LeapDays
- }
-
- # date2day("yy/mm/dd") returns the number of days that passed from
- # 1970 Jan 1 to the given date. -1 is returned on error.
- # The fields are returned in Fields: year in Fields[1], month in Fields[2],
- # and day (if given) in Fields[3].
- # If the day is not given, the first of the month is used.
- function date2day(Date,Fields, Num,Year,Month) {
- Num = split(Date,Fields,"/")
- if (Num != 2 && Num != 3)
- return -1
- if (!(Year = Fields[1] + 0) || !(Month = Fields[2] + 0))
- return -1
- if (Num == 3)
- Day = Fields[3]
- return YMD2day(Year,Month,Num == 3 ? Day : 1)
- }
-
- # diffdays(year1,month1,day-of-month1,year2,month2,day-of-month2)
- # returns the number of complete days that passed from date 1 to date 2
- function diffdays(year1,month1,day1,year2,month2,day2) {
- return date2days(year2,month2,day2) - date2days(year1,month1,day1)
- }
-
- # Given an epoch month, return the first day of that month
- function month2day(Month) {
- return YMD2day(int(Month/12) + 1970,Month % 12 + 1,1)
- }
-
- # Given an epoch day, returns the epoch day of the last day of that month
- function endOfMonth(Day, Month) {
- Month = day2month(Day)
- return month2day(Month) + monthdays(Month) - 1
- }
-
- # Given an epoch day, returns epoch month
- function day2month(Day, Date) {
- day2YMD(Day,Date)
- return (Date["y"]-1970)*12 + Date["m"]-1
- }
-
- # Given an epoch month, returns the number of days in that month.
- function monthdays(month, year) {
- if (!(0 in MDur))
- split("31 28 31 30 31 30 31 31 30 31 30 31",MDur)
- year = int(month/12)
- month = month%12+1
- return (!((year+2)%4) && month == 2) ? 29 : MDur[month]
- }
-
- # Given an epoch day (day since 1970 Jan 1; day 0 = 1970 Jan 1, etc.),
- # returns the date elements in Date:
- # Date["y"] = year (4 digits), Date["m"] = month (jan = 1, etc.),
- # Date["d"] = day of month.
- # Globals: Sets/uses MDays[].
- function day2YMD(Day,Date, QYears,Year,NonLeapYears,Month) {
- if (!(0 in LDays)) {
- split("0 31 59 90 120 151 181 212 243 273 304 334 365",MDays," ")
- split("0 31 60 91 121 152 182 213 244 274 305 335 366",LDays," ")
- }
- Day += 365
- # Day is now # of days since Jan 1 1969. 1968 was a leap year.
- QYears = int(Day / (365*4+1))
- Year = 1969 + QYears * 4
- Day -= QYears * (365*4+1)
- # Day now contains no complete leap years.
- Year += NonLeapYears = int(Day/365)
- Leap = !(Year % 4)
- Day -= NonLeapYears * 365
- # Day now contains the day of year.
- # Find the month. Divide day by 32 to get either the correct month or
- # the month prior to it.
- Month = int(Day++ / 32) + 1
- if (Day > (Leap ? LDays[Month+1] : MDays[Month+1]))
- Month++
- Day -= Leap ? LDays[Month] : MDays[Month]
- Date["d"] = Day
- Date["m"] = Month
- Date["y"] = Year
- }
-
- # Given a month number, return a date in the form yy/mm
- function month2date(MonthNum) {
- return sprintf("%02d/%02d",(MonthNum / 12 + 70) % 100, MonthNum % 12 + 1)
- }
-
- # Given a day number, return a date in the form yy/mm/dd
- function day2date(day) {
- day2YMD(day,Date)
- return sprintf("%02d/%02d/%02d",Date["y"]%100,Date["m"],Date["d"])
- }
-
- ### End date-days routines
-
- function ErrExit(S) {
- if (!FNR)
- # If a failure occurs before any lines are read, ignore the string
- # passed and print errno instead.
- printf "Could not read file \"%s\": %s\n",FILENAME,ERRNO
- else
- printf "Error on line %d of file \"%s\":\n%s\n^^^ %s\n",
- FNR,FILENAME,$0,S > "/dev/stderr"
- exit 1
- }
-
- function min(a,b) {
- if (a < b)
- return a
- else
- return b
- }
-
- ### 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
-