#!/usr/skunk/bin/gawk -f # @(#) mlast.gawk 1.1 94/06/16 # 93/03/14 john h. dubois iii (john@armory.com) # 93/12/03 Process last -w /etc/utmp first so that last doesn't have to # search through huge wtmp for users who have been logged in a # long time. Sort output to print most recent first. # Added command line options. # 93/12/19 Use gawk for strftime() and /dev/stderr # Removed uninteresting fields from last output & added age field # 93/12/31 Added x option # 94/01/22 Fixed sorting # 94/03/13 Exit 0 from command for gawk. Close all files. # 94/04/25 Added l and o options, header # 94/05/10 Added Old field # 94/06/06 Added L option BEGIN { Name = "mlast" Usage = "Usage: " Name " [-hnox] [-l tty,...] [-L tty,...]" ARGC = Opts(Name,Usage,"nl:L:ohxt:k",0,"/etc/default/mlast", "NOSORT,TTYS,IGNORETTYS,NOHEADER",0,"k") if ("h" in Options) { print \ Name ": show last logins on enabled modem lines.\n"\ "Options:\n"\ "-h: Print this help.\n"\ "-n: Show logins as they are found, with no sorting. This lets output be\n"\ " seen without waiting a long time for " Name " to find the wtmp entry\n"\ " for a modem line that hasn't been logged into for a long time.\n"\ "-l : Search for last logins on the ttys given in the\n"\ " comma- or space-separated list. -t is a synonym for -l.\n"\ "-L : Do not report on the named ttys even if they are enabled.\n"\ "-o: Do not print header.\n"\ "-x: Print debugging info." exit 0 } if (ARGC > 1) { print Usage | "cat 1>&2" exit 1 } Year = strftime("%Y") # TZ() MkMonth2Num() Sort = !("n" in Options) Debug = "x" in Options CurTime = systime() if (Debug) printf "Current time: %d\n",CurTime if ("l" in Options) NumGettys = MakeSet(GettyLines,Options["l"],"[, ]+") else if ("t" in Options) NumGettys = MakeSet(GettyLines,Options["t"],"[, ]+") else { NumGettys = FindMGettys(GettyLines) if ("L" in Options) { MakeSet(NonGettyLines,Options["L"],"[, ]+") for (tty in NonGettyLines) if (tty in GettyLines) { delete GettyLines[tty] NumGettys-- } } } if (Debug) { printf "Searching for:" for (Line in GettyLines) printf " %s",Line print "" } Format = "%-9s %-8s %16s %-21s %11s %9s" if (!("o" in Options)) printf Format "\n","User","TTY","Login date","Dur","Age","Old" split("/usr/bin/last -w /etc/utmp:/usr/bin/last",Cmds,":") for (i = 1; i in Cmds; i++) if (!(NumGettys = \ ProcLast(Cmds[i],GettyLines,NumGettys,Last,Ages,Format))) break if (Sort) { if (Debug) { printf "Sorting ages:" for (i in Ages) printf " %s",Ages[i] print "" } qsort_arb_ind(Ages,k) if (Debug) print "Done sorting." for (i = 1; i in k; i++) print Last[k[i]] } } # Last output #User Line Device PID Login time Elapsed Time Comments #rstevew 3C tty3C 26585 Sun Mar 14 04:05 00:21 logged in function ProcLast(Cmd,GettyLines,NumGettys,Last,Ages,Format, Device,Month,DayOfMonth,TimeElem,Fields,Line,Age,Time,Old) { # Exit 0 for gawk # Timezone is wiped out so last will print in GMT, to avoid having to # figure daylight savings when comparing last time to current time. Cmd = "TZ= " Cmd "; exit 0" while (Cmd | getline) { Device = $3 if (Device in GettyLines) { Month = $6 DayOfMonth = $7 split($8,TimeElem,":") Ages[Device] = Time = \ unixtime(Year,Month2Num[Month],DayOfMonth,TimeElem[1], TimeElem[2],0,TZOffset) # Split duration into hours & minutes split($9,TimeElem,":") Old = CurTime - (Time + TimeElem[1] * 3600 + TimeElem[2] * 60) # Skip over User, Line, Device, PID, and date match($0, "^[^ ]+ +[^ ]+ +[^ ]+ +[^ ]+ +[^ ]+ +[^ ]+ +[^ ]+ +[^ ]+ +") Fields = substr($0,RLENGTH + 1) Age = sec2dhm(CurTime - Time) if (Old+0 >= 60) Old = sec2dhm(Old) else Old = "" # Because last login time is given for GMT, must recreate it # with timezone taken into consideration # user tty login date Line = sprintf(Format,$1,Device,strftime("%a %b %d %H:%M",Time), Fields,Age,Old) if (Debug) { printf "Found record for %s in output of \"%s\":\n%s\n", Device,Cmd,$0 > "/dev/stderr" printf "Date: %d (%s), age: %d (%s)\n",Time, strftime("%c",Time),CurTime - Time,Age > "/dev/stderr" } if (!Sort) print Line Last[Device] = Line delete GettyLines[Device] # $2 = "" if (!--NumGettys) break if (Debug) { printf "Still searching for %d entries:",NumGettys for (Device in GettyLines) printf " %s",Device print "" } } } close(Cmd) return NumGettys } # Getty lines: #01:2345:respawn:/etc/getty tty01 sc_m #3A:23:respawn:/usr/lib/uucp/uugetty -t60 tty3A 3 function FindGettys(GettyLines, Line,F,Elem,NumGettys,Cmd,File) { File = "/etc/inittab" while ((getline Line < File) == 1) { if (Line ~ "^#") continue split(Line,F,":") if (F[3] == "respawn" && F[4] ~ "^[^ \t]*getty ") { split(F[4],Cmd,"[ \t]+") for (Elem = 2; Elem in Cmd; Elem++) if (Cmd[Elem] ~ "(^|/)tty") { GettyLines[Cmd[Elem]] NumGettys++ break } } } close(File) return NumGettys } function FindMGettys(GettyLines, Elem,NumGettys) { NumGettys = FindGettys(GettyLines) for (Elem in GettyLines) if (Elem !~ "[A-Z]") { delete GettyLines[Elem] NumGettys-- } return NumGettys } # Arr is an array of values with arbitrary indices. # Array k is returned with numeric indices 1..n. # The values in k are the indices of array arr, # ordered so that if array 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 array (n). function qsort_arb_ind(arr,k, ArrInd,end) { end = 0 for (ArrInd in arr) k[++end] = ArrInd; qsortseg(arr,k,1,end); return end } # @(#) qsortseg.awk 2.0 94/01/20 # Non-recursive qsort. Slightly slower than recursive qsort, # but gawk 2.15.3 chokes on recursive version with internal error. # 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 qsortseg(arr,k,start,end, left,right,sepval,tmp,tmpe,tmps,ind,val,S,E,stackptr) { stackptr = 0 S[0] = start E[0] = end while (stackptr >= 0) { # pop values from stack left = start = S[stackptr] right = end = E[stackptr] stackptr-- # handle two-element case explicitely for a tiny speedup if ((end - start) == 1) { if (arr[tmps = k[start]] > arr[tmpe = k[end]]) { k[start] = tmpe k[end] = tmps } continue } 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) { S[++stackptr] = start E[stackptr] = right } if (left < end) { S[++stackptr] = left E[stackptr] = end } } } # date2days(year,month,day-of-month) # returns the number of complete days that passed from 1900 Jan 1 # to the start of the given date. # All parameters should be given in numeric form. # Works from 1901 to 2099. # If year < 100, it is assumed to be part of the 1900 century # Globals: sets and uses MDays[] function date2days(Year,Month,Day, LeapDays) { Year += 0 Month += 0 if (Year > 100) Year -= 1900 LeapDays = int(Year/4) + 1 if (Month <= 2 && Year % 4 == 0) LeapDays -= 1 if (!MDays[2]) split("0 31 59 90 120 151 181 212 243 273 304 334 365",MDays) return Year * 365 + MDays[Month + 0] + Day - 1 + LeapDays } # unixdays(year,month,day-of-month) # returns the number of complete days that passed from 1970 Jan 1 # to the start of the given date function unixdays(Year,Month,Day) { return date2days(Year,Month,Day) - 25568 } # unixtime(year,month,day-of-month,hour,minute,second,timezone) # returns the number of seconds that passed from 1970 Jan 1 00:00:00 # to the given date. # Timezone should be given as a numeric hour offset from GMT function unixtime(Year,Month,Day,Hour,Minute,Second,Timezone) { return (((date2days(Year,Month,Day) - 25568) * 24 + Hour + Timezone) \ * 60 + Minute) * 60 + Second } # 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) } # Set global TZOffset to numeric timezone #function TZ() { # if (!("TZ" in ENVIRON)) # TZOffset = 0 # else { # TZOffset = ENVIRON["TZ"] # gsub("[^0-9]","",TZOffset) # } #} # date2unixtime returns the number of seconds that passed from # 1970 Jan 1 00:00:00 to the given date. # Globals: Sets/uses TZOffset and MDays[] function date2unixtime(Year,Month,Day, LeapDays) { # if (TZOffset == "") # TZ() if (Year > 100) Year -= 1900 LeapDays = int((Year - 68) / 4) if (Month <= 2 && Year % 4 == 0) LeapDays -= 1 if (!MDays[2]) split("0 31 59 90 120 151 181 212 243 273 304 334 365",MDays," ") return (((Year - 70) * 365 + MDays[Month + 0] + Day - 1 + LeapDays) * 24 \ + TZOffset) * 3600 } function MkMonth2Num( Month) { split("Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec",Months,",") for (Month in Months) Month2Num[Months[Month]] = sprintf("%02d",Month) } function sec2dhms2(Seconds, Days,Hours,Minutes,Time) { Days = int(Seconds / 86400) Seconds %= 86400 Hours = int(Seconds / 3600) Seconds %= 3600 Minutes = int(Seconds / 60) Seconds %= 60 if (Days) Time = Days "d " if (Time || Hours) Time = Time sprintf("%02d",Hours) ":" if (Time || Minutes) Time = Time sprintf("%02d",Minutes) ":" Time = Time sprintf("%02d",Seconds) return Time } function sec2dhm(Seconds, Days,Hours,Minutes,Time) { Days = int(Seconds / 86400) Seconds %= 86400 Hours = int(Seconds / 3600) Seconds %= 3600 Minutes = int(Seconds / 60) if (Days) Time = Days "d " Time = Time sprintf("%02d:%02d",Hours,Minutes) return Time } # 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 } # @(#) ProcArgs 1.2.1 94/06/11 # 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. # 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 # * 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 ":". # 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 bug that caused problems in some cases. # 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 "1". # 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. # 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. # 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 -1 is returned. function ProcArgs(argc,argv,OptList,Options,compress, ArgNum,ArgsLeft,Arg,ArgLen,ArgInd,Option,Pos,NumOpt,Value,HadValue, NeedNextOpt) { # 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,"&") for (ArgNum = 1; ArgNum < argc; ArgNum++) { if ((Arg = argv[ArgNum]) !~ /^[-+]./) # Not an option; quit break delete argv[ArgNum] ArgsLeft-- if ((Arg == "--") || (Arg == "++")) break ArgLen = length(Arg) for (ArgInd = 2; ArgInd <= ArgLen; ArgInd++) { Option = substr(Arg,ArgInd,1) if (NumOpt && Option ~ /[-+.0-9]/) { Option = "&" Arg = "&" Arg ArgLen++ Pos = NumOpt } else if (!(Pos = index(OptList,Option)) || Option == "&") { OptErr = "Invalid option: -" Option return -1 } # Find what the value of the option will be if it needs one if (NeedNextOpt = (ArgInd >= ArgLen)) # Value is the next arg Value = argv[ArgNum+1] else # Value is included with option Value = substr(Arg,ArgInd + 1) if (HadValue = AssignVal(Option,Value,Options, substr(OptList,Pos + 1,1),ArgNum < (argc - 1))) { if (HadValue == -1) return -1 if (NeedNextOpt) { delete argv[++ArgNum] ArgsLeft-- } break # Used up this option } } } if (compress != 0) PackArr(argv,ArgsLeft) return ArgsLeft } # Global variables: OptErr # Return value: -1 on error, 0 if option did not require an argument, # 1 if it did. function AssignVal(Option,Value,Options,ArgType,GotValue,Name, UsedValue,Err) { # If option takes a value... if (UsedValue = (ArgType ~ "[:*()#<>]")) { 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)) != "") { OptErr = Err return -1 } # Mark this as a numeric variable; will be propogated to Options[] val. if (ArgType != ":") Value += 0 } else Value = 1 if (!(Option in Options)) # Do not overwrite previously assigned values Options[Option] = Value 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 # * Floating point argument # ( Non-negative floating point argument # ) Positive floating point argument # # Integer argument # < Non-negative integer argument # > Positive integer argument # Returns null on success, err string on error function CheckType(ArgType,Value,Option,Name, Err) { if (ArgType == ":") return "" # A number begins with option + or -, and is followed by a string of # digits or a decimal with digits before it, after it, or both 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 != "") { if (Name != "") return "Value assigned to variable " Name " " Err else { if (Option == "&") Option = Value return "Value assigned to option -" Option " " Err } } else return "" } # Packs Arr to indices starting with 0 # Num should be the number of elements in Arr function PackArr(Arr,Num, NewInd,OldInd) { NewInd = OldInd = 0 for (; Num; Num--) { while (!(OldInd in Arr)) OldInd++ if (NewInd != OldInd) { Arr[NewInd] = Arr[OldInd] delete Arr[OldInd] } OldInd++ NewInd++ } } # 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. # rcFile, if given, is the name of a file to read for variable initialization. # Values given in it will not override values given on the command line. # 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 UseEnv is given and nonzero, the variables will also be searched for in # the environment. Values given in the environment will override those given # in the rcfile 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 rcfile from being read. # 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 1 so it can be checked for in an END block. # Return value: The number of elements left in ARGV is returned. function Opts(Name,Usage,OptList,MinArgs,rcFile,VarNames,UseEnv,NoRCopt, ArgsLeft) { if (MinArgs == "") MinArgs = 0 ArgsLeft = ProcArgs(ARGC,ARGV,OptList NoRCopt,Options,1) if ((ArgsLeft + ("h" in Options)) < (MinArgs+1)) { if (ArgsLeft != -1) OptErr = "Not enough arguments" print Name ": " OptErr ". Use -h for help." print Usage Err = 1 exit 1 } if (rcFile != "" && (NoRCopt == "" || !(NoRCopt in Options)) && InitOpts(rcFile,Options,OptList,VarNames,UseEnv) == -1) { print Name ": " OptErr ". Use -h for help." Err = 1 exit 1 } return ArgsLeft } # Global vars: sets OptErr; uses ENVIRON[] function InitOpts(rcFile,Options,OptTypes,VarNames,UseEnv, Line,Var,Pos,Vars,Map,CharOpt,NumVars,TypesInd,Types,Type,Ret) { NumVars = split(VarNames,Vars,",") TypesInd = Ret = 0 for (i = 1; i <= NumVars; i++) { Var = Vars[i] CharOpt = substr(OptTypes,++TypesInd,1) if (CharOpt ~ "^[:*()#<>&]$") CharOpt = substr(OptTypes,++TypesInd,1) Map[Var] = CharOpt Types[Var] = Type = substr(OptTypes,TypesInd+1,1) # Do not overwrite entries from environment if (UseEnv && Var in ENVIRON && AssignVal(CharOpt,ENVIRON[Var],Options, Type,1,Var) == -1) return -1 } if (rcFile ~ "^~/") rcFile = ENVIRON["HOME"] substr(rcFile,2) while ((getline Line < rcFile) == 1) if (Line !~ /^#/ && Line !~ "^[ \t]*$") { if (Pos = index(Line,"=")) Var = substr(Line,1,Pos-1) else Var = Line # If no value, var is entire line if (Var in Map) { if (AssignVal(Map[Var],substr(Line,Pos+1),Options, Types[Var],Pos != 0,Var) == -1) return -1 } else { OptErr = sprintf("Unknown var \"%s\" set in %s",Var,rcFile) Ret = -1 } } if ("x" in Options) for (Var in Map) if (Map[Var] in Options) printf "%s=%s\n",Var,Options[Map[Var]] else printf "%s not set\n",Var return Ret }