#!/usr/local/bin/gawk -f #!/usr/bin/awk -f # @(#) oldusers.gawk 4.0 97/02/07 # 92/08/10 John H. DuBois III (john@armory.com) # 92/11/07 rewrote; fixed to work with a large number of users # 92/11/26 Do not report users who have not logged in for 6 mo. if preserved # 93/03/18 Use gawk to get strftime(). Made k[] global to work around gawk bug. # Added option. Other general improvements. # 93/07/18 Rewrite with new, sensible options etc. # 93/12/13 Allow comments in preserved users file & check for nonexistant # users in it. # 94/05/12 Added I option # 94/08/28 Added checking of login shells. # 95/04/15 Avoid timezone issues by working with days instead of seconds. # 95/09/12 Added -rtp options. Let a list of users be given on command line. # 96/01/03 Ported to 5.0: deal with different 'l' message for file not found. # Fixed s/b options mixup. # 96/11/14 Read rcfile. Added N and L options. # If no users of a type, report the oldest of that type. # Get account creation time from shell rcfiles instead of home dir, # 97/02/07 Added M option. Check for pop file as well as .lastlogin. BEGIN { Name = "oldusers" Usage = \ "Usage: " Name " [-bhnlrtsxp] [-u] [-i] [-dNL]\n"\ " [-M [user ...]" Days = 365 MinUID = 100 DefIgnFile = "/usr/local/lib/preserve_users" rcFile = "/etc/default/" Name ARGC = Opts(Name,Usage,"nlbdhx>rt",0,rcFile, "NEVERONLY,LONGONLY,BRIEF,DAYS,MINUID,IGNOREFILE,NOSHELLTEST,PROGRESS,"\ "NEVERDAYS,LONGDAYS,MAXEXECARGS") if ("h" in Options) { printf \ "%s: find users who have never logged in,\n"\ "and find users who have not logged in for a specified period.\n"\ "%s\n"\ "Users who have not logged in for a given number of days and/or users who\n"\ "have never logged in and whose accounts were created the given number of\n"\ "days or more ago are listed. The default is %d days. If usernames are\n"\ "given, only those users are checked, user are checked regardless of uid\n"\ "unless -u is given, and the meaning of the -s and the -I options is\n"\ "inverted: they are turned on by default; giving them turns them off.\n"\ "If user names are not given on the command line, all users are checked.\n"\ "In order to determine who is a real user and who is a pseudo-user (and\n"\ "should therefore be ignored), the file /etc/shells is read. If it exists,\n"\ "it is taken to be a list of all of the real-user login shells. Any user\n"\ "whose login shell does not appear in it is ignored. Also, regardless of\n"\ "whether /etc/shells exists, any user who has no login shell specified is\n"\ "ignored. In addition, any user whose uid is less than a specified uid\n"\ "(see the -u option) is ignored.\n"\ "The last login time is taken from the modification time of the user's\n"\ ".lastlogin and /usr/spool/mail/..pop file, whichever is more\n"\ "recent. The account creation time is taken from the modification time of\n"\ "the user's shell startup files.\n"\ "Options:\n"\ "Some of the following options can also be set by assigning values to\n"\ "variables in a configuration file named %s. Variables are\n"\ "assigned to with the syntax: varname=value or in the case of flags, by\n"\ "simply putting the indicated variable name in the file without a value.\n"\ "Options given on the command line override assigments in the configuration\n"\ "file. Flag options set in the configuration file can be turned off on the\n"\ "command line by following them immediately with \"-\", e.g. -v- to turn\n"\ "off the v option in such a way that it cannot be turned on in the config\n"\ "file. Variable names appear in parentheses in the option descriptions.\n"\ "-h: print this help.\n"\ "-n: Print never-logged-in users only. (NEVERONLY)\n"\ "-l: Print long-time-since-login users only. (LONGONLY)\n"\ "-b: Brief form: print login names only, without headers. (BRIEF)\n"\ "-x: Turn on debugging at level , which should be a positive\n"\ " integer. Higher values cause more verbose diagnostics to be printed.\n"\ "-M: Set the maximum number of characters on a command line to\n"\ " . The default is 1000. If the operating system supports\n"\ " a longer command line, setting it here will speed things up.\n"\ "-d: Set the # of days at which a user becomes an old user. (DAYS)\n"\ "-N, -L: Like -d, but apply to never-logged-in users and\n"\ " long-time-since-login users only respectively, and override -d for\n"\ " these purposes. (NEVERDAYS, LONGDAYS)\n"\ "-u: Users with uids less than are ignored (default: %d).\n"\ " (MINUID)\n"\ "-i: Users listed in are ignored. The default is\n"\ " %s. Use -i \"\" or IGNOREFILE= to not have any\n"\ " ignorefile read. (IGNOREFILE)\n"\ "-r: Read a list of users to check from the standard input, one per line.\n"\ " Each line may include \"tag\" information, separated from the username\n"\ " by whitespace. The tag is printed after the username. The -sI\n"\ " options are treated as when usernames are given on the command line.\n"\ "-t: Sort by the tag rather than the time. This turns on the r option.\n"\ "-s: Do not ignore users who have no login shell or whose login shell does\n"\ " not appear in /etc/shells. (NOSHELLTEST)\n"\ "-p: Print a progress report. (PROGRESS)\n", Name,Usage,Days,rcFile,MinUID,DefIgnFile exit 0 } Brief = Options["b"] # Short form (names only) LoggedInOnly = "l" in Options # Don't report on never-logged-in users NeverLoggedInOnly = "n" in Options # Report only on never-logged-in users if ("M" in Options) MAXEXECARGS = Options["M"] ChkShell = !("s"in Options) if ("x" in Options) { Debug = Options["x"] printf "Debugging enabled at level %d\n",Debug > "/dev/stderr" } Verbose = "p" in Options || Debug TagSort = "t" in Options # sort by tag value rather than tiem ReadInput = "r" in Options || TagSort if ("d" in Options) Days = Options["d"] NeverDays = "N" in Options ? Options["N"] : Days LongDays = "L" in Options ? Options["L"] : Days if ("i" in Options) DefIgnFile = Options["i"] ReadIgnore = (DefIgnFile != "") if ("u" in Options) MinUID = Options["u"] if (ARGC > 1) { UsersGiven = 1 for (i = 1; i < ARGC; i++) UserTags[ARGV[i]] } if (ReadInput) { if (Verbose) print "Reading specified-users list..." > "/dev/stderr" UsersGiven = 1 while (getline) if (NF) { User = $1 if (NF > 1) { Tag = $0 sub(/^[^ \t]+[ \t]+/,"",Tag) } else Tag = "" UserTags[User] = Tag } } if (UsersGiven) { ReadIgnore = !ReadIgnore ChkShell = !ChkShell MinUID = -1 } # Make the indexes of PreserveUsers[] be the set preserved users if (ReadIgnore) { if (Verbose) print "Reading ignored-users list..." > "/dev/stderr" while ((err = (getline user < DefIgnFile)) == 1) if (user !~ /^#/) { gsub("[ \t].*","",user) # get rid of whitespace/comments PreserveUsers[user] } } if (err == -1 && DefIgnFile != "") { printf "Error reading ignored users file \"%s\"\n", DefIgnFile > "/dev/stderr" exit(1) } # Initialize month name/number lookup tables MkMonth2Num() Num = 0 # Ignore users without real-user login shells. if (ChkShell) { if (Verbose) print "Reading login shells list..." > "/dev/stderr" ReadShells(Shells) } for (User in UserTags) Users[User] CurDay = int(systime()/(24*3600)) ChkPaths["~/.lastlogin"] ChkPaths["/usr/spool/mail/.%u.pop"] GetTimes(MinUID,PreserveUsers,ChkShell,Shells,UsersGiven,Users,Verbose, NeverDays,LongDays,LoggedInOnly,LLUsers,NLUsers,CurDay,ChkPaths) for (User in PreserveUsers) printf "Preserved user not found in passwd database: %s\n", User > "/dev/stderr" for (User in Users) printf "Specified user not found in passwd database: %s\n", User > "/dev/stderr" if (!LoggedInOnly) { Report(UserTags,NLUsers, "Users created " NeverDays " or more days ago and never logged in:", TagSort,Brief,CurDay) } if (!NeverLoggedInOnly) { if (!LoggedInOnly && !Brief) print "" Report(UserTags,LLUsers, "Users who have not logged in for " LongDays " days or more:", TagSort,Brief,CurDay) } } # GetTimes: find last login times for users. # MinUID: minimum UID of users to check (0 if users should be checked # regardless of UID). # PreserveUsers[]: set of account names to ignore. # ChkShell: true if only users with a shell in Shells[] shell should be checked # Shells[]: set of shells that real users have, if ChkShell is true. # UsersGiven: true if only users in Users should be checked. # Users[]: set of user names to check, if UsersGiven is true. # Verbose: true if actions should be printed. # NeverDays: Age that home dir may reach before never logged into account is # considered old. # LongDays: Age that a login timestamp file may reach before account is # considered old. # LoggedInOnly: True if only need check on users who logged in at least once. # Returns values: # All users found are deleted from Users[] and PreserveUsers[]. # The following return times as UNIX epoch day, indexed by user: # LLUsers[]: For users who have logged in, last login time. # NLUsers[]: For users who have never logged in, account creation time. # CurDay: Current day, in UNIX epoch format. # ChkPaths: Set of paths to check for each user. For each path, the ~ in a # leading ~/ is replaced by the user's home directory, and the first instance # of %u is replaced by the user's account name. # The index OLDEST is also set in each array, if there is any user of the given # type; its value is the timestamp and username of the oldest account of the # given type (even if that user is not put in the array due to the thresholds), # separated by a semicolon. function GetTimes(MinUID,PreserveUsers,ChkShell,Shells,UsersGiven,Users, Verbose,NeverDays,LongDays,LoggedInOnly,LLUsers,NLUsers,CurDay,ChkPaths, PWEnt,User,LLFiles,oldestLogin,oldestCreated,path,File2User,Date, MinDate,UserFiles) { if (Debug) printf "GetTime(): NeverDays = %d; LongDays = %d\n",NeverDays, LongDays > "/dev/stderr" split("",PWEnt) # let gawk know this is an array if (Verbose) print "Reading password file..." > "/dev/stderr" while (getpwent(PWEnt)) { # Ignore system users User = PWEnt[PW_NAME] if (!(PWEnt[PW_UID] < MinUID || User in PreserveUsers || ChkShell && !(PWEnt[PW_SHELL] in Shells)) && (!UsersGiven || User in Users)) LLUsers[User] delete PreserveUsers[User] delete Users[User] } if (Verbose) print "Checking last login times..." > "/dev/stderr" # Find users who have not logged in in a long time, # and who do not have login timestamp files at all. GetUserTimestamps(LLUsers,ChkPaths,1,UserFiles) MinDate = CurDay - LongDays for (User in LLUsers) { Date = LLUsers[User] if (Date == "") { # Never logged in NLUsers[User] delete LLUsers[User] continue } if (oldestLogin == "" || Date < oldestLogin) { oldestLogin = LLUsers[User] oldestUser = User } if (Debug > 5) printf "timestamp for %8s: %d; file used: %s\n", User,LLUsers[User],UserFiles[User] > "/dev/stderr" if (Date > MinDate) # Has logged in in the given time period delete LLUsers[User] } if (oldestLogin != "") LLUsers["OLDEST"] = oldestLogin ";" oldestUser if (!LoggedInOnly) { if (Verbose) print "Checking account creation times..." > "/dev/stderr" oldestCreated = FindWhenCreated(NeverDays,NLUsers,CurDay - NeverDays) if (oldestCreated != "") NLUsers["OLDEST"] = oldestCreated } } # For each user in Users[], each of the paths in ChkPaths[] is convert to a # path for a user-specific file. For each user, the newest or oldest file is # found (depending on whether Newest is true). The modification time of the # file is returned as the value for that user's index in Users[], as a UNIX # epoch day. If none of the files exist for a user, the value of that user's # index is not modified. # ChkPaths: Set of paths to check for each user. For each path, the ~ in a # leading ~/ is replaced by the user's home directory, and the first instance # of %u is replaced by the user's account name. # The name of the selected file for each user is returned in UserFiles[], # index by user name. function GetUserTimestamps(Users,ChkPaths,Newest,UserFiles, LLFiles,path,User,PWEnt,File2User,NotFound,File,Date) { for (User in Users) if (getpwnam(User,PWEnt)) for (path in ChkPaths) { sub("^~/",PWEnt[PW_HOME] "/",path) sub("%u",User,path) LLFiles[path] File2User[path] = User } else printf "GetUserTimestamps(): No such user: %s\n", User > "/dev/stderr" GetFileDates(LLFiles,NotFound) for (File in LLFiles) { if (!(File in File2User)) { printf "GetUserTimestamps(): file %s not in File2User?!\n", File > "/dev/stderr" continue } if (File in NotFound) continue User = File2User[File] if (!(User in Users)) { printf "GetUserTimestamps(): User %s not in Users?!\n", User,File > "/dev/stderr" continue } Date = LLFiles[File] if (Date == "") { printf "GetUserTimestamps(): No output from 'l' for file %s.\n", File > "/dev/stderr" continue } if (Users[User] == "" || Newest && Date > Users[User] || \ !Newest && Date < Users[User]) { Users[User] = Date UserFiles[User] = File } } } # Put a list of login shells (from /etc/shells) into set LoginShells[]. # Returns -1 if /etc/shells could not be read, else the number of shells found. function ReadShells(LoginShells, ret,Num,Line) { while (ret = ((getline Line < "/etc/shells") == 1)) if (Line ~ "^/") { Num++ sub(/[ \t]+/,"",Line) LoginShells[Line] } close("/etc/shells") return ret ? -1 : Num } # UserTags[] gives the user names to be reporting on, with the value being # the tag for the user if given. # Users[username] gives the date, as a UNIX day, since the account # was created or last logged into # Header: Header line to be printed. # TagSort: True if users should be sorted by tag value instead of time. # Brief: Print only user names. # Global vars: Debug function Report(UserTags,Users,Header,TagSort,Brief,CurDay, k,User,TagLen,Format,i,TagField,Num,oldest,elem) { if (!Brief) print Header if ("OLDEST" in Users) { oldest = Users["OLDEST"] delete Users["OLDEST"] # make NotEmpty(Users) work } if (!NotEmpty(Users)) { if (!Brief) if (!split(oldest,elem,";")) print "None." else printf "None. Oldest user is %s; age is %d days.\n",elem[2], CurDay - elem[1] return } # Accumulate longest tag length, for later formatting. for (i in UserTags) if (length(UserTags[i]) > TagLen) { TagLen = length(UserTags[i]) if (Debug) printf "Tag for %s is <%s>\n",i,UserTags[i] > "/dev/stderr" } # Build sort keys if (TagSort) { for (i in UserTags) if (!(i in Users)) delete UserTags[i] Num = qsortArbIndByValue(UserTags,k) } else Num = qsortArbIndByValue(Users,k) if (TagLen) TagField = "%-" TagLen "s " Format = "%-8s " TagField "%s %s\n" for (i = 1; i <= Num; i++) { User = k[i] if (Brief) print User else { if (getpwnam(User,PWEnt)) { if (TagLen) printf Format,User,UserTags[User], day2date(Users[User]),PWEnt[PW_GCOS] else printf Format,User,day2date(Users[User]),PWEnt[PW_GCOS] } else printf \ "Error: user %s not found in passwd database.\n", User > "/dev/stderr" } } } # The creation date for each account that is an index of NLUsers[] # is determined & stored (as a epoch day) as the value for the index. # The time that gives the date that a never-logged-into account was created is # determined from the timestamp on one of the user's shell rcfiles. The # timestamp on the home directory is not used because it is touched whenever a # filesystem restore is done, etc. # Global vars: Debug # Return value: the timestamp of the oldest account found (as an epoch day) # and the owner of the account, separated by a semicolon. function FindWhenCreated(Days,NLUsers,MinDate, File,User,HDNotFound,i,Num,oldest,UsersNotFound,ShellFiles,UserFiles, oldestLogin) { if (Debug) printf "FindWhenCreated(): Days = %d\n",NeverDays, LongDays > "/dev/stderr" ShellFiles["~/.profile"] ShellFiles["~/.zprofile"] ShellFiles["~/.bash_profile"] ShellFiles["~/.login"] GetUserTimestamps(NLUsers,ShellFiles,Oldest,UserFiles) for (User in NLUsers) { Date = NLUsers[User] if (Date == "") { # No rcfiles UsersNotFound[User] delete NLUsers[User] continue } if (oldestLogin == "" || Date < oldestLogin) { oldestLogin = NLUsers[User] oldest = oldestLogin ";" User } if (Debug > 5) printf "timestamp for %8s: %d; file used: %s\n", User,NLUsers[User],UserFiles[User] > "/dev/stderr" if (Date > MinDate) # Created more recently than cutoff date delete NLUsers[User] } if (NotEmpty(UsersNotFound)) { printf "No shell rcfiles found for these users:\n" > "/dev/stderr" for (i in UsersNotFound) print i > "/dev/stderr" } return oldest } # GetFileDates: Get the timestamp of files, as UNIX epoch date. # Files[] is an array whose indices give the files to check. # Files that are not found are made indices of NotFound[]. # The modification time of each file found is stored under its index in Files[] # (this will fail if a filename contained wildcards and is expanded by the # shell). # Globals: # Month2Num[] must be set up # If MAXEXECARGS is set, it is the maximum length of a command line. # Otherwise, the default of 1000 is used. # Debug turns on debugging if true. function GetFileDates(Files,NotFound, Month,uday,LFiles,Ind,E,Year,File,LOut,i,oldest,MaxArgs,len,Cmd) { oldest = "" Ind = 0 # Do l -d in case a file is a dir Cmd = "exec l -d" MaxArgs = ((MAXEXECARGS > 0) ? MAXEXECARGS : 1000) - length(Cmd) - 5 len = 0 for (File in Files) { # Avoid exceeding command line length limit by doing l in batches len += length(File) + 1 # Always put at least one file on command line, even if its length is # greater than MaxArgs if (len > MaxArgs && LFiles != "") { Ind = DoCmd(Cmd LFiles " 2>&1",Ind + 1,LOut) LFiles = " " File len = length(LFiles) } else LFiles = LFiles " " File } if (LFiles != "") Ind = DoCmd(Cmd LFiles " 2>&1 || :",Ind + 1,LOut) for (i in LOut) { if (split(LOut[i],E," +") < 3) { if (Debug) printf "i" > "/dev/stderr" continue } # Put the names of files that do not exist in NotFound[] # 3.2v4 'l' says 'foo not found'; 3.2v5 'l' says # 'l: foo not found: No such file or directory (error 2)' if (E[3] == "found" || E[4] == "found:") { if (Debug) printf "n" > "/dev/stderr" NotFound[E[E[3] == "found" ? 1 : 2]] continue } #l output formats: #-r-------- 1 root auth 0 Mar 18 16:08 /.lastlogin #-r-------- 1 caity auth 0 Sep 07 1992 /u/caity/.lastlogin # Allow a day of -1; epoch time 0 may result in that due to timezone # differences. if ((uday = lDate2unixday(E[6],E[7],E[8])) < -1) { printf \ "Error: Bad date conversion of <%s,%s,%s> (result: %d) from:\n%s\n", E[6],E[7],E[8],uday,LOut[i] > "/dev/stderr" continue } File = E[9] if (!(File in Files)) { printf \ "Error: Unknown filename <%s> in:\n%s\n", File,LOut[i] > "/dev/stderr" continue } Files[File] = uday if (uday < 1 && Debug) printf "Suspiciously old file: Epoch day = %d. l line:\n%s\n", uday,LOut > "/dev/stderr" } if (Debug) print "" > "/dev/stderr" } function DoCmd(Cmd,i,Arr, oi) { Cmd = Cmd "; exit 0" # gawk complains about this oi = i while ((Cmd | getline Arr[i++]) == 1) ; close(Cmd) if (Debug) printf "Got %d lines (tot %3i) from %s\n",i - oi,i-1, substr(Cmd,1,50) > "/dev/stderr" return i - 1 } # Make Months[n] be the name of month number n (starting with 0) # and Month2Num[Month-Abbrev] the number of the month with 3-letter # abbreviation Month-Abbrev. 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 abort(s) { print s exit(1) } ### Begin date-days routines # @(#) date-days 1.1 95/08/26 # 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[] # Returns negative value on error. function YMD2day(Year,Month,Day, LeapDays) { if (Month !~ /^[0-9]+$/ || Day !~ /^[0-9]+$/) return -1 Year+=0 Month+=0 Day+=0 if (Month <= 0 || Day <= 0 || Year < 0 || Day > 31 || Month > 12) return -2 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]. 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,Day) } # 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 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, Date) { day2YMD(day,Date) return sprintf("%02d/%02d/%02d",Date["y"]%100,Date["m"],Date["d"]) } function lDate2unixday(Mon,Day,Year, Month) { if (!(1 in Month2Num)) { MkMonth2Num() CurYear = strftime("%y") CurMonth = strftime("%m") } Month = Month2Num[Mon] # Deal with varying dates printed by l # Use year if given # Subtract 1 from year if month given is from last year if (Year ~ ":") # If year is actually time... Year = (CurYear - (Month > CurMonth)) % 100 return YMD2day(Year,Month,Day) } ### End date-days routines function NotEmpty(Arr, i) { for (i in Arr) break return (i in Arr) } ### 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 # . # 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 ### Begin pwent library # @(#) pwent.awk 1.2 96/06/27 # 92/08/10 john h. dubois III (john@armory.com) # 93/12/13 fixed to not clobber $* # 96/01/05 Send error messages to /dev/stderr # 96/05/24 Let getpwnam() return a specific field if requested. # Added PW_REAL and PW_OFFICE. # 96/06/03 Added Type field to getpwent() # 96/06/24 Allow a Field to be requested for getpwent() also. # 96/06/29 Added PW_RECORD, and getpwreal(). # Changed PWLines to be index by record number instead of name. # 96/11/17 Added getpwuid() # Require: ReadShells() # 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 record number, # starting with 1. # _pwNames[] is a mapping of name to passwd record number. # 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( User,Line,i,Ind,ret,OFS) { if (PW_Name) return 1 PW_NAME = 1 PW_PASSWORD = 2 PW_UID = 3 PW_GID = 4 PW_GCOS = 5 PW_HOME = 6 PW_SHELL = 7 PW_REAL = -1 # for PWGetFields() PW_OFFICE = -2 PW_RECORD = -3 Ind = getpwentNum = 1 OFS = FS FS = ":" while ((ret = (getline Line < "/etc/passwd")) == 1) { User = Line sub(":.*","",User) _pwNames[User] = Ind PWLines[Ind++] = Line } FS = OFS close("/etc/passwd") if (ret) { printf "ReadPasswd(): Could not open /etc/passwd.\n" > "/dev/stderr" return 0 } return 1 } # setpwent resets the passwd file entry pointer used by getpwent # to the first entry. function setpwent() { getpwentNum = 1 } # getpwent sets PWEnt to the next entry in the passwd file. # If Type is set to -1, the entry for the next "real" user is returned (others # are skipped over), where a real user is a user whose login shell is listed in # /etc/shells. This requires the ReadShells() function. Other values for # Type are not yet defined and are ignored. # If the last entry has already been returned, 0 is return if Field is null, # ":" if not. # If the entry for the next real user has been requested and /etc/shells # cannot be read, -1 is returned if Field is null, "\n" if not. # See PWGetFields() for other return values and the meaning of the Field # parameter. function getpwent(PWEnt,Type,Field, entNum) { if (!PW_NAME) ReadPasswd() if (!(getpwentNum in PWLines)) return Field ? ":" : 0 if (Type == -1) { if (!_DidReadShells && ReadShells(LoginShells) == -1) return Field ? "\n" : -1 split(PWLines[getpwentNum++],PWEnt,":") while (!(PWEnt[PW_SHELL] in LoginShells)) { if (!(getpwentNum in PWLines)) return Field ? ":" : 0 split(PWLines[getpwentNum++],PWEnt,":") } return PWGetFields("",PWEnt,Field,getpwentNum - 1) } else { entNum = getpwentNum return PWGetFields(PWLines[getpwentNum++],PWEnt,Field,entNum) } } function MakeInd( Elem,Ind,Line,uid,home) { for (Ind = 1; Ind in PWLines; Ind++) { Line = PWLines[Ind] split(Line,Elem,":") uid = Elem[PW_UID] if (!(uid in uidInd)) uidInd[uid] = Ind home = Elem[PW_HOME] if (!(home in HomeInd)) HomeInd[home] = Ind } IndDone = 1 } # PWGetFields() splits PWLine into PWEnt[], and optionally returns a field # from it. If PWLine is null, PWEnt[] is assumed to have already been filled # in with a password entry. # If Field is not passed or is null, the return value is 1. # If Field is non-null, it should a PW_ value. In this case, the value of the # requested field is returned. # entNum is the value that PWEnt[PW_RECORD] should be set to. It should be # the index in PWLines[] of the record being processed. # In addition to the PW_ values used by the rest of the functions in this # library, this function can be passed PW_REAL and PW_OFFICE. # PW_REAL will get the part of the GCOS field before the first comma. # PW_OFFICE will get the part of the GCOS field after the first comma. # If either of these is requested, both values will also be assigned to their # indices in PWEnt[], unless there is no comma in the GCOS field, in which case # PW_OFFICE will not be set. # NOTE: since the global field names are set in ReadShells(), it must be # executed before any of the field name can be passed. function PWGetFields(PWLine,PWEnt,Field,entNum, gcos,ind) { if (PWLine != "") split(PWLine,PWEnt,":") PWEnt[PW_RECORD] = entNum if (!Field) return 1 if (Field < 0) { if (ind = index(gcos = PWEnt[PW_GCOS],",")) { PWEnt[PW_OFFICE] = substr(gcos,ind+1) PWEnt[PW_REAL] = substr(gcos,1,ind-1) } else PWEnt[PW_REAL] = gcos } return PWEnt[Field] } # getpwnam sets PWEnt to the passwd entry for login name Name. # If Name does not exist in the password file, the return value is ":" # if Field was passed, 0 if not. # For other return values and parameter explanation, see PWGetFields() function getpwnam(Name,PWEnt,Field) { if (!PW_NAME) ReadPasswd() if (Name in _pwNames) return PWGetFields(PWLines[_pwNames[Name]],PWEnt,Field,_pwNames[Name]) else return Field ? ":" : 0 } # getpwhome sets PWEnt to the passwd entry whose home dir is Home. # See getpwnam() for return values and the meaning of the Field param. function getpwhome(Home,PWEnt,Field) { if (!PW_NAME) ReadPasswd() if (!IndDone) MakeInd() if (Home in HomeInd) return PWGetFields(PWLines[HomeInd[Home]],PWEnt,Field,HomeInd[Home]) else return Field ? ":" : 0 } # getpwuid sets PWEnt to the passwd entry whose uid is UID. # See getpwnam() for return values and the meaning of the Field param. function getpwuid(UID,PWEnt,Field) { if (!PW_NAME) ReadPasswd() if (!IndDone) MakeInd() if ((UID + 0) in uidInd) return PWGetFields(PWLines[uidInd[UID]],PWEnt,Field,uidInd[UID]) else return Field ? ":" : 0 } # Make an index by real name. For each passwd file entry, the real-name # is lowercased and split into components on non-alphanums. The passwd entry # index that the name came from is added to the value of each such component # in the global _RealInd[]. The indexes stored this way are separated by # commas. If the real-name contains no alphanums, its index is stored under # the null index. function _makeRealInd( PWEnt,ret,Elem,nelem,i,Component) { setpwent() while ((ret = getpwent(PWEnt,"",PW_REAL)) != ":") { nelem = split(tolower(ret),Elem,/[^a-z0-9]+/) for (i = 1; i <= nelem; i++) { Component = Elem[i] if (Component == "" && nelem > 1) continue if (Component in _RealInd) _RealInd[Component] = _RealInd[Component] "," PWEnt[PW_RECORD] else _RealInd[Component] = PWEnt[PW_RECORD] } } _realIndDone = 1 } # Make Name into a pattern that will match a name that contains all of the # same name components (sequences of alphanums) in the same order. If Name # contains no name components, a null string is returned. function MakeNamePat(Name, Elem,nelem,i,Pat,e) { nelem = split(Name,Elem,/[^a-zA-Z0-9]+/) for (i = 1; i <= nelem; i++) { if ((e = Elem[i]) == "") continue if (Pat == "") Pat = "(^|[^a-zA-Z0-9])" e else Pat = Pat "[^a-zA-Z0-9](.*[^a-zA-Z0-9])?" e } if (Pat == "") # If Name contained no alphanums... return "" Pat = Pat "([^a-zA-Z0-9]|$)" return Pat } # getpwgreal sets PWEnt to the first passwd entry whose PW_REAL (see # PWGetFields()) field matches Real. Matching occurs if the alphanumeric # components of Real occur in the same order in the entry. Non-alphanums are # ignored. All of the components in Real must occur in the entry, but not all # of the components in the entry must occur in Real. # If the given name does not exist in the password file, # the return value is ":" if Field was passed, 0 if not. # If Next is true, getpwreal() sets PWEnt to the next passwd entry whose # PW_REAL field matches the last previous Real parameter passed. # In this case, if the last entry has already been returned, # the return value is ":" if Field was passed, 0 if not. # Different IgnoreCase and Full parameters may be given when doing a Next # search. Both must always be passed; they do not default to the original # values when doing a Next search. The only parameter ignored when doing a # Next search is Real. # If IgnoreCase is true, case is ignored when searching. # If Full is true, a match of the full name is required (including any # punctuation). # For successful return values and Field parameter explanation, # see PWGetFields() # Globals: For the Next search, between invokations these varies store values: # _getpwrealInd[]: The set of pw indices that matched the query. # _getpwrealIndInd: The next index in _getpwrealInd[] to look at. # _getpwrealReal: The Real value passed with the original query. # _getpwrealPat: Real converted to a component order search pattern. function getpwreal(Real,PWEnt,Field,IgnoreCase,Full,Next, ind,name,Pat) { if (!Next) { if (!PW_NAME) ReadPasswd() if (!_realIndDone) _makeRealInd() _getpwrealReal = Real _getpwrealPat = MakeNamePat(Real) # Get first component from Real Real = tolower(Real) gsub("^[^a-z0-9]+","",Real) gsub("[^a-z0-9].*","",Real) if (!(Real in _RealInd)) return Field ? ":" : 0 split(_RealInd[Real],_getpwrealInd,",") _getpwrealIndInd = 1 } if (Full) Pat = _getpwrealReal else Pat = _getpwrealPat if (IgnoreCase) Pat = tolower(Pat) while (_getpwrealIndInd in _getpwrealInd) { ind = _getpwrealInd[_getpwrealIndInd++] name = PWGetFields(PWLines[ind],PWEnt,PW_REAL,ind) if (IgnoreCase) name = tolower(name) if (Full ? (name == Pat) : (name ~ Pat)) return PWGetFields("",PWEnt,Field,ind) } return Field ? ":" : 0 } ### End pwent library ### 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