#!/usr/skunk/bin/gawk -f #!/usr/skunk/bin/gawk -f # Use gawk for strftime() # @(#) lastlogin.gawk 1.0 94/02/09 # 93/04/20 john h. dubois iii (john@armory.com) # 93/05/12 Deal with unstatable and nonexistant .lastlogin files. # 94/01/14 Added all options. # 94/02/08 Added u option and check for bogus dates; use mod time instead of # access time on .lastlogin; use name of user running program if # no names given; fixed bug that let only one person per last-time # be reported on if sorting is done. # 94/02/09 Added l option, and explicit exit 0 from stat command to avoid # gawk error message. BEGIN { Name = "lastlogin" Usage = "Usage: " Name " [-ahils] user ..." ARGC = Opts(Name,Usage,"alshi",0) if ("h" in Options) { printf \ Name ": show last login times of users.\n"\ Usage "\n"\ "Options:\n"\ "-a: Report time of last login attempt, whether successful or not.\n"\ "-h: Print this help.\n"\ "-i: Read names of users to report on from the standard input.\n"\ " Multiple lines of input and multiple names per line may be given.\n"\ "-l: Report last successful login (the default).\n"\ " -la can be used to get both reports.\n"\ "-s: Sort output by last login time.\n" exit 0 } ReadInput = "i" in Options Sort = "s" in Options Attempt = "a" in Options Successful = "l" in Options || !Attempt HalfYearAgo = systime() - 182 * 24 * 3600 if (!ReadInput && ARGC < 2) { ARGC = 2 ARGV[1] = id() } if (ARGC > 1) FindLastLogins(ARGC,ARGV,Sort,Attempt,Successful,Lines,HalfYearAgo) if (ReadInput) { while ((ret = (getline < "/dev/stdin")) == 1) { for (i = 1; i <= NF; i++) ARGV[i] = $i FindLastLogins(i,ARGV,Sort,Attempt,Successful,Lines,HalfYearAgo) } if (ret) { print "Error reading stdin" > "/dev/stderr" } } if (Sort) { Num = qsort_by_index(Lines,k) for (i = 1; i <= Num; i++) print Lines[k[i]] } } function FindLastLogins(ARGC,ARGV,Sort,Unsuc,Suc,Lines,HalfYearAgo, i,User,PWEnt,Home,Files,Cmd,LTime,Access,Modify,Create,Line) { for (i = 1; i < ARGC; i++) { User = ARGV[i] if (getpwnam(User,PWEnt)) { Home = PWEnt[PW_HOME] Home2User[Home] = User Files = Files " " Home "/.lastlogin" } else printf "%s: %s: No such user.\n",Name,User > "/dev/stderr" } if (Files == "") return Cmd = "stat '-c ' -nfnamc " Files " 2>&1; exit 0" while ((Cmd | getline) == 1) { if (!NF) continue Home = $1 Access = $2 Modify = $3 Create = $4 sub("/.lastlogin.*","",Home) User = Home2User[Home] if (Access !~ "^[0-9]+$") { if ($0 ~ "No such file") printf "%s: Never logged in.\n",User else { sub("^[^ ]* ","") printf "Cannot stat %s's .lastlogin file: %s\n",User,$0 \ > "/dev/stderr" } continue } if (!Suc) LTime = Create else LTime = Modify # files with dates like these seem to occasionally be legitimately created # if (Access != Modify - 2) # printf ".lastlogin dates for %s appear to be bogus.\n",User \ # > "/dev/stderr" Line = sprintf("%-10s %s",User,FmtTime(LTime,HalfYearAgo)) if (Unsuc && Suc) Line = Line " " FmtTime(Create,HalfYearAgo) if (Sort) { # If this time index is already use, add a newline before # appending another line if (LTime in Lines) Lines[LTime] = Lines[LTime] "\n" Lines[LTime] = Lines[LTime] Line } else print Line } close(Cmd) } function FmtTime(Time,HalfYearAgo) { if (Time < HalfYearAgo) return strftime("%a %b %d %Y",Time) else return strftime("%a %b %d %H:%M",Time) } # 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 indices. # The return value is the number of elements in the array (n). function qsort_by_index(arr,k, ArrInd,end) { end = 0 for (ArrInd in arr) k[++end] = ArrInd; qsort_num_ind(k,1,end); return end } # qsort_num_ind: sort an array # arr is the array to be sorted. # It must have contiguous numeric indexes. # start and end are the starting and ending indexes of the range to be sorted. function qsort_num_ind(arr,start,end, left,right,sepval,tmp,tmpe,tmps) { # handle two-element case explicitely for a tiny speedup if ((start - end) == 1) { if ((tmps = arr[start]) > (tmpe = arr[end])) { arr[start] = tmpe arr[end] = tmps } return } left = start right = end sepval = arr[int((left + right + 0.5) / 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) qsort_num_ind(arr,start,right) if (left < end) qsort_num_ind(arr,left,end) } # getpwent, getpwnam: get an entry from the passwd file. # Each of the following passwd functions returns an array which contains # a passwd file entry. The array contains the fields of the entry. # Global variables: # The following variables are defined with the values of the indexes of the # entries: PW_NAME, PW_PASSWORD, PW_UID, PW_GID, PW_GCOS, PW_HOME, PW_SHELL # PWLines[] contains the lines of the password file, indexed by user name. # PWInd[] is a mapping of passwd entry number to acct name. # getpwentNum is the number of the next entry to be returned by getpwent(). # Left FS global because making it local does not work in gawk. function ReadPasswd( i,Ind,ret,OFS) { PW_NAME = 1 PW_PASSWORD = 2 PW_UID = 3 PW_GID = 4 PW_GCOS = 5 PW_HOME = 6 PW_SHELL = 7 Ind = getpwentNum = 1 OFS = FS FS = ":" while ((ret = (getline < "/etc/passwd")) == 1) { PWLines[$1] = $0 PWInd[Ind++] = $1 } FS = OFS close("/etc/passwd") if (ret) { printf "ReadPasswd(): Could not open /etc/passwd.\n" > "/dev/stderr" return 0 } } # getpwnam sets PWEnt to the passwd entry for login name Name. # If Name does not exist in the passwd file, 0 is returned. # Otherwise, 1 is returned. function getpwnam(Name,PWEnt, FieldNum) { if (!PW_NAME) ReadPasswd() if (Name in PWLines) { split(PWLines[Name],PWEnt,":") return 1 } return 0 } # getpwent sets PWEnt to the next entry in the passwd file. # If the last entry has already been returned, 0 is returned. # Otherwise, 1 is returned. function getpwent(PWEnt) { if (!PW_NAME) ReadPasswd() if (!(getpwentNum in PWInd)) return 0 SetPWEntry(PWInd[getpwentNum++],PWEnt) return 1 } # @(#) ProcArgs 1.1 94/01/01 # 92/02/29 john h. dubois iii # 93/07/18 Added "#" arg type # 93/09/26 Don't count -h against MinArgs # 94/01/01 Stop scanning at first non-option arg. Added '>' option type. # Removed meaning of '+' or '-' by itself. # optlist is a string which contains all of the possible command line options. # If a character is followed by a colon, # it indicates that that option takes an argument. # If a character is followed by a pound sign (#), # it indicates that that option takes an integer argument. # 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[]. # If an option takes an argument, the argument may either immedately # follow it or be given separately. # 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. # An argument of "--" or "++" stops the scanning of argv[]. # The number of arguments left in argc is returned. # If an error occurs, the 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) { # 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. ArgsLeft = argc for (ArgNum = 1; ArgNum < argc; ArgNum++) { if ((Arg = argv[ArgNum]) !~ /^[-+]./) break delete argv[ArgNum] ArgsLeft-- if ((Arg == "--") || (Arg == "++")) break ArgLen = length(Arg) for (ArgInd = 2; ArgInd <= ArgLen; ArgInd++) { Option = substr(Arg,ArgInd,1) Pos = index(optlist,Option) if (!Pos) { OptErr = "Invalid option: -" Option return -1 } # If option takes a value... if ((ArgType = substr(optlist,Pos + 1,1)) ~ "[:#>]") { if (ArgInd < ArgLen) # Value is included with option options[Option] = substr(Arg,ArgInd + 1) else { # Value is the next arg after option if (ArgNum < (argc - 1)) { options[Option] = argv[++ArgNum] delete argv[ArgNum] ArgsLeft-- } else { OptErr = "Option -" Option " requires an argument" return -1 } } if (ArgType == "#" && options[Option] !~ "^-?[0-9]+$") { OptErr = \ "Option -" Option " requires an integer argument" return -1 } else if (ArgType == ">" && ( options[Option] !~ "^[0-9]+$" || options[Option] ~ "^0+$")) { OptErr = \ "Option -" Option " requires a positive integer argument" return -1 } break # Used up this option } else options[Option] = 1 } } if (compress != 0) PackArr(argv,ArgsLeft) return ArgsLeft } # 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++ } } # 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. # 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, ArgsLeft) { if (MinArgs == "") MinArgs = 0 ArgsLeft = ProcArgs(ARGC,ARGV,OptList,Options,1) if ((ArgsLeft + ("h" in Options)) < (MinArgs+1)) { if (ArgsLeft != -1) OptErr = "Not enough arguments" print Name ": " OptErr ". Use -h for help." > "/dev/stderr" print Usage > "/dev/stderr" Err = 1 exit 1 } return ArgsLeft } # id returns the login name of the user who owns the current process function id( Cmd,line,elem) { Cmd = "/usr/bin/id" Cmd | getline line split(line,elem,"[()]") close(Cmd) return elem[2] }