:
# try to find and use a reasonably sane and sufficiently non-ancient perl:
eval '
	# we do not want to use shell variables / named parameters here,
	# as those could conflict with environment variables,
	# so that makes this code a bit longer than it might otherwise be

	# first try what (if anything) our PATH finds first
	if >>/dev/null 2>&1 perl -e "use strict; use Fcntl; use Config; use Getopt::Long; Getopt::Long::Configure qw(posix_default bundling);"; then
		#that seems good - go for it
		exec perl -S "$0" ${1+"$@"}
	else
		# and if that does not work, then ...
		case X`uname -s` in
			# for flavors that do not provide (non-ancient) Perl with the
			# operating system, try /usr/local/bin/perl next
			# for completeness, these should probably be tweaked for various
			# uname -s and uname -r versions, and other version/revision factors
			XHP-UX|XSunOS)
				if >>/dev/null 2>&1 /usr/local/bin/perl -e "use strict; use Fcntl; use Config; use Getopt::Long; Getopt::Long::Configure qw(posix_default bundling);"; then
					exec /usr/local/bin/perl -S "$0" ${1+"$@"}
				elif >>/dev/null 2>&1 /usr/bin/perl -e "use strict; use Fcntl; use Config; use Getopt::Long; Getopt::Long::Configure qw(posix_default bundling);"; then
					exec /usr/bin/perl -S "$0" ${1+"$@"}
				fi
			;;
			# for flavors that do provide (non-ancient) Perl with the
			# operating system, try /usr/bin/perl next
			XLinux|XBIG-IP|*)
				if >>/dev/null 2>&1 /usr/bin/perl -e "use strict; use Fcntl; use Config; use Getopt::Long; Getopt::Long::Configure qw(posix_default bundling);"; then
					exec /usr/bin/perl -S "$0" ${1+"$@"}
				elif >>/dev/null 2>&1 /usr/local/bin/perl -e "use strict; use Fcntl; use Config; use Getopt::Long; Getopt::Long::Configure qw(posix_default bundling);"; then
					exec /usr/local/bin/perl -S "$0" ${1+"$@"}
				fi
			;;
		esac
		# if we succeeded above, the use of exec should render this unreachable:
		1>&2 echo "$0: failed to find good perl, aborting."
		exit 1
	fi
'
	if $running_under_some_shell;

# and then the real perl code starts ...

#for vi(1) :-) :
#se tabstop=4 shiftwidth=4 autoindent redraw showmatch showmode

use strict;

#enable warnings
$^W=1;

#set a secure umask
defined (umask 077) or die ("$0: \"umask 077\" failed, aborting");

#lowest UID number we allow to be disabled by default
my $minUID=50;

# maximum number of seconds we sleep attempting to get a lock
my $max_lock_wait_seconds=15;

# do we allow setting an empty password hash? (boolean)
my $allowemptypasswordhash=undef;

my $locked_home_prefix='/LOCKED,was';

# various signal sets we use:
my @signals_nice=(15,1,9);
my @signals_nasty=(9);

my $signal_sleep=1; # seconds to sleep between sending signals

# sanity check $locked_home_prefix
if(
	$locked_home_prefix =~
	m!
		^$ # disallow empty
		|
		:	# disallow colon
		|
		\n	# disallow newline
		|
		\0	# disallow null
		|
		^[^/]	#disallow starting with non-/
		|
		/$	#disallow ending with /
	!ox
){
	die "$0: \$locked_home_prefix must start with /, not end with /, and cannot contain :, newline, or null\n";
};

# we apparently need Fcntl (or FileHandle) for sysopen, O_EXCL, etc.
use Fcntl;
#use FileHandle;

# we'll need this later for mapping signal numbers to names
use Config;
defined $Config{sig_name} ||
	die "$0: unable to determine signal names via use Config;\n";
my @signal_names=(split(' ', $Config{sig_name}));

# from options and option arguments
my $force;
my $help;
my $nochange;
my $restore_directory;
my $save_directory;
my @uid;

my $usage=<<""
usage: $0 [-f|--force] [-h|-?|--help] [-n|--nochange] [-r directory|--restore directory] [-s directory|--save directory] [-u uid|--uid uid] [-v [level]|--verbose [level]] [login ...]
	-f|--force
		be forceful - bypass or relax some checks
	-h|-?|--help
		help - provide some basic usage information, overrides other options
		except verbose
	-n|--nochange
		no account change actions - do not alter accounts or signal PIDs
	-r directory|--restore directory
		restore - reverse most effects of disabling where --save was used
	-s directory|--save directory
		save data to directory so --restore may later be used
	-u uid|--uid uid
		specify uid, signals PIDs, may be given multiple times, does not
		itself trigger processing of any associated login(s)
	-v [level]|--verbose [level]
		be verbose, optionally specifying verbosity level
	login
		login name

;

# detabify
$usage=~s/\t/    /og;

use Getopt::Long;

Getopt::Long::Configure (

	# start with "sane", conservitive POSIX (compatible) defaults
	"posix_default",

	# then tweak as seems fitting
	"bundling"

);

# we start with a reference level of -1, as --verbose takes optional
# argument, and will be set to 0 if --verbose is given but optional
# argument isn't supplied
my $verbose=-1;

GetOptions
	(	"force|f" => \$force,
		"help|h|?" => \$help,
		"nochange|n" => \$nochange,
		"restore|r=s" => \$restore_directory,
		"save|s=s" => \$save_directory,
		"uid|u=i" => \@uid,
		"verbose|v:i" => \$verbose
	) or die
		(	"$0: bad option(s), aborting\n",
			"$usage",
			"aborting"
		)
;

# since we started with a reference level of -1, we want to increment
# this to have a more intuitive value
++$verbose;
if($verbose<0){
	#we no longer have use for $verbose to be negative
	$verbose=0;
};
# this also allows us to conveniently check the boolean state of $verbose

# --verbose					optarg		$verbose	results
# <option not specified>	N/A			0
#							<=-1		0
#							0 or <none>	1			quite high level comments
#							1			2			per-login/uid
#							2			3			actions per-login/uid
#							5			6			internal details
#							N>=0		N+1

if(defined($help)){
	# --help is mutually exclusive of other options and arguments
	if(
		defined($force) ||
		defined($nochange) ||
		defined($restore_directory) ||
		defined($save_directory) ||
		@uid ||
		@ARGV ||
		$verbose
	){
		warn "$0: --help is mutually exclusive of other options and arguments\n";
		die "$usage";
	}else{
		exit !(print "$usage");
	};
};

if(defined($restore_directory)&&(defined($save_directory)||@uid||@ARGV)){
	die (
		"$0: --restore is mutually exclusive of login arguments and these options: --save, --uid\n",
		"$usage"
	);
};

# Check that we're superuser, or that we'll have no real work to do.
# Non-superuser use/attempt would only be valid if we have no real work
# to do (--nochange is specified, or there are no logins and no uids
# specified and no $restore_directory)
unless($nochange||(!@ARGV&&!@uid&&!defined($restore_directory))){
	$> == 0 or die "$0: must be superuser (uid 0, a.k.a. \"root\"), aborting.\n"
};

my $error=0;		# track if we got error(s) for later non-zero exit

sub my_mkdir{
	$verbose>=6 && print ($0,': sub my_mkdir(',join(',',@_),")\n");
	# last argument is directory,
	# preceeding arguments are options
	my $p=undef;	# if true, similar to mkdir(1) -p option - make
					# parent direcories as necessary

	my $o_excl=1;	# if true, similar to O_EXCL for penultimate directory

	my @dirstuff=@_;
	my $dir=pop(@dirstuff);
	if(!length($dir) || $dir =~ /\0/o){
		# we need to have gotten a string which is non-null and contains
		# no nulls
		warn "$0: sub my_mkdir: directory must be defined and not be or contain nulls.\n";
		return undef;
	};
	while(defined(my $opt=pop(@dirstuff))){
		if($opt =~ /^-?(p|r|-?(parents?|recursive))$/o){
			$p=1;
		}elsif($opt =~ /^-{0,2}exists?(_ok)?$/o){
			$o_excl=0;
		}else{
			warn "$0: sub my_mkdir: don't understand option: \"$opt\".\n";
			return undef;
		};
	};

	if(!$p){
		if($o_excl){
			return(mkdir($dir,0777));
		}else{
			return(-d $dir || mkdir($dir,0777));
		};
	};

	# we may need to make parent directory(/ies)

	# split at one or more slashes
	@dirstuff=split(m!/+!o,"$dir");
	# this also effectively removes trailing slashes

	unless(defined($dirstuff[0])&& length($dirstuff[0])){
		# if we're dealing with absolute pathname
		# stick / in front as first directory element
		$dirstuff[0]='/';
	};

	# do roughly the equivalent of a mkdir(1) -p
	if(!$#dirstuff){
		# only a single component of directory pathname (no possible
		# parent(s) to make)
		if($o_excl){
			return(mkdir($dirstuff[0],0777));
		}else{
			return(-d $dirstuff[0] || mkdir($dirstuff[0],0777));
		};
	};

	# we have parent(s) to potentially make

	my $j=join('/',@dirstuff[0..$#dirstuff-1]);

	# colapse multiple consecutive / characters to a single /
	$j =~ s!/{2,}!/!og;

	# handle the parent (recursively as necessary)
	-d "$j" || &my_mkdir('-p',$j) or return;

	# handle the penultimate directory
	if($o_excl){
		return(mkdir($dir,0777));
	}else{
		return(-d $dir || mkdir($dir,0777));
	};
};

################################################################################
# Operating System specific PARAMeterS (variables)							   #
################################################################################
# we need to know these for dealing with login(s)							   #
my $true=undef;				# pathname for true program						   #
my $lockedshell=undef;		# pathname for locked shell program				   #
																			   #
my $passwd_lock;			# lock file for /etc/passwd						   #
my $passwd=undef;			# pathname for /etc/passwd file or equivalent	   #
																			   #
							# $shadow and $shadow_lock are undef for systems   #
							# that use alternative mechanisms (e.g. TCB)	   #
my $shadow_lock=undef;		# lock file for $shadow							   #
my $shadow=undef;			# pathname of /etc/shadow file (must match format) #
my $shadow_or_tcb=undef;	# do we use shadow file, or tcb file?			   #
																			   #
my $unmatchable_pwhash='*';	# string to use for unmatchable(/locked) password  #
							# we give it a "sane" default value, "just in case"#
																			   #
my $cron_allow=undef;		# cron.allow file pathname						   #
my $cron_deny=undef;		# cron.deny file pathname						   #
my $at_allow=undef;			# at.allow file pathname						   #
my $at_deny=undef;			# at.deny file pathname							   #
my $ftpusers=undef;			# ftpusers file pathname						   #
my $list_crontab_needs_allow=undef;	# allow needed to list					   #
my $list_crontab_args=undef;	# anonymous sub ref to compose arguments	   #
my $remove_crontab_needs_allow=undef;	# allow needed to remove			   #
my $remove_crontab_args=undef;	# anonymous sub ref to compose arguments	   #
my $restore_crontab_args=undef;	# anonymous sub ref to compose arguments	   #
my $crontabs_dir=undef;		# directory of crontabs							   #
my $cron_vixie_cruft=undef; # listing crontab includes Vixie cruft			   #
my $get_at_jobs=undef;		# anonymous sub ref to get user's at jobs		   #
my $remove_at_job=undef;	# anonymous sub ref to remove at job			   #
################################################################################
# Operating System specific PARAMeterS (subroutine to set above variables)	   #
# it succeeds, or die()s													   #
################################################################################

sub ck_osparms{
	$verbose>=6 && print ($0,': sub ck_osparms(',join(',',@_),")\n");
	# we need to have all these defined and set non-null to be okay
	#temporarily disable warnings
	local $^W=0;
	for my $ck (
		$true,
		$lockedshell,
		$passwd_lock,
		$passwd,
		$shadow_or_tcb,
		$unmatchable_pwhash,
		$cron_allow,
		$cron_deny,
		$at_allow,
		$at_deny,
		$ftpusers,
		$list_crontab_needs_allow,
		$list_crontab_args,
		$remove_crontab_needs_allow,
		$remove_crontab_args,
		$restore_crontab_args,
		$crontabs_dir,
		$cron_vixie_cruft,
		$get_at_jobs,
		$remove_at_job
	){
		unless(length($ck)){
			return undef;
		}
	};
	return 1;
};

sub osparams_sunos_5__6_8_10_common{
	$passwd_lock='/etc/ptmp';
	$passwd='/etc/passwd';
	$unmatchable_pwhash='*';
	$shadow='/etc/shadow';
	$shadow_lock='/etc/stmp';
	$shadow_or_tcb='shadow';
	$cron_allow='/etc/cron.d/cron.allow';
	$cron_deny='/etc/cron.d/cron.deny';
	$at_allow='/etc/cron.d/at.allow';
	$at_deny='/etc/cron.d/at.deny';
	$list_crontab_needs_allow=1;
	$list_crontab_args=sub {
		return('/usr/bin/crontab','-l',$_[0]);
	};
	$remove_crontab_needs_allow=$list_crontab_needs_allow;
	$remove_crontab_args=sub {
		return('/usr/bin/crontab','-r',$_[0]);
	};
	$restore_crontab_args=sub {
		return('/usr/bin/crontab');
	};
	$crontabs_dir='/var/spool/cron/crontabs';
	$cron_vixie_cruft=0;
	$get_at_jobs=sub {
		$verbose>=6 && print ($0,': sub $get_at_jobs(',join(',',@_),")\n");
		unless(length($_[0])){
			return undef;
		};
		my $login=$_[0];
		my @user_at_jobs=();

		my $chpid=open(GET_AT_JOBS, '-|');
		if(!defined($chpid)){
			#(implicit) fork failed
			warn "$0: open(GET_AT_JOBS, '-|') failed, $!.\n";
			return undef;
		}elsif(!$chpid){
			#I'm child
			exec {'/usr/bin/at'} ('at','-l');
			die "$0: exec ('/usr/bin/at','-l') failed, $!, child aborting.\n";
		#else #I'm parent
		};
		while(<GET_AT_JOBS>){
			chomp;
			if(s/^user = \Q$login\E\s+(\S+)\s+\w+\s+\w+\s+\d?\d\s+\d?\d:\d\d:\d\d\s+\d{4}$/\1/){
				push @user_at_jobs,$_;
			};
		};
		unless(close(GET_AT_JOBS)){
			warn "$0: close(GET_AT_JOBS) failed.\n";
			return undef;
		};
		$verbose>=6 && print ("$0: sub ",'$get_at_jobs: @user_at_jobs=(',join(',',@user_at_jobs),")\n");
		return \@user_at_jobs;
	};
	$remove_at_job=sub {
		$verbose>=6 && print ($0,': sub $remove_at_job(',join(',',@_),")\n");
		my $ret=system {'/usr/bin/at'} ('at','-r',$_[0]);
		if($ret){
			warn ("$0: system ('/usr/bin/at','-r',$_[0]) failed",($ret==-1?(": $!"):(", returned: " . ($ret>>8))),"\n");
			$error=1;
			return undef;
		};
		return 1;
	};
};

sub suse_8_1_nscd_stop{
	# stop SuSE 8.1 nscd if it's there and seems to be running
	! -x '/etc/init.d/nscd' ||
	! -x '/usr/sbin/nscd' ||
	system(
			'/bin/sh',
			'-c',
			# some basic security initialization and such
			'cd / && umask 022 && ' .
			# quietly determine if it's in use:
			'>>/dev/null 2>&1 /bin/fuser /usr/sbin/nscd && ' .
			# and if so, stop it:
			'exec /etc/init.d/nscd stop'
	);
	# we don't particularly care about the return/exit value(s) from
	# our above system()
	return 1;
};

sub osparms{
	$verbose>=6 && print ($0,': sub osparms(',join(',',@_),")\n");
	my $uname_sr;
	$uname_sr=qx'uname -sr';

	if(!defined($uname_sr)){
		die "$0: \"uname -sr\" failed, aborting.\n";
	};
	chomp $uname_sr;

	# find pathname for true - is it /bin/true or /usr/bin/true?
	if(-l '/bin' && -x '/usr/bin/true'){
		$true='/usr/bin/true';
	}elsif(-x '/bin/true'){
		$true='/bin/true';
	}else{
		die "$0: can't find pathname for true program, aborting";
	};
	$lockedshell=$true;

	if($uname_sr =~ /^Linux\b/){
		#that's nice, but we need to know the operating system, not the kernel

		my $release_file;
		if(
			(	$release_file='/etc/redhat-release',
					open(RELEASE,,$release_file)
			)
			||
			(
				$release_file='/etc/SuSE-release',
				open(RELEASE,,$release_file)
			)
		){
			{
				local $/=undef;	# quite temporarily use slurp mode
				$_=<RELEASE>;	# slurp
				$/='';	# quite temporarily use paragraph mode
				chomp;
			};
			unless(close(RELEASE)){
				die "$0: failed to close $release_file, aborting";
			};
			if(
				m!
					^Red\ Hat\ 
						(
							Enterprise\ Linux\ [AE]S\ release\ 3\ 
							\(Taroon\ Update\ [3-6]
							|
							Linux\ Advanced\ Server\ 
							release\ 2.1AS\ \(Pensacola
							|
							Linux\ release\ 7.3\ \(Valhalla
						)
					\)$
				!ox
			){
				$passwd_lock='/etc/ptmp';
				$passwd='/etc/passwd';
				$shadow_lock='/etc/sptmp';
				$shadow='/etc/shadow';
				$shadow_or_tcb='shadow';
				$unmatchable_pwhash='!';
				$cron_allow='/etc/cron.allow';
				$cron_deny='/etc/cron.deny';
				$at_allow='/etc/at.allow';
				$at_deny='/etc/at.deny';
				$ftpusers='/etc/ftpusers';
				$list_crontab_needs_allow=0;
				$list_crontab_args=sub {
					return('/usr/bin/crontab','-u',$_[0],'-l');
				};
				$remove_crontab_needs_allow=$list_crontab_needs_allow;
				$remove_crontab_args=sub {
					return('/usr/bin/crontab','-u',$_[0],'-r');
				};
				$restore_crontab_args=sub {
					return('/usr/bin/crontab','-');
				};
				$crontabs_dir='/var/spool/cron';
				$cron_vixie_cruft=0;
				$get_at_jobs=sub {
					$verbose>=6 && print ($0,': sub $get_at_jobs(',join(',',@_),")\n");
					unless(length($_[0])){
						return undef;
					};
					my $login=$_[0];
					my @user_at_jobs=();

					my $chpid=open(GET_AT_JOBS, '-|');
					if(!defined($chpid)){
						#(implicit) fork failed
						warn "$0: open(GET_AT_JOBS, '-|') failed, $!.\n";
						return undef;
					}elsif(!$chpid){
						#I'm child
						exec {'/usr/bin/at'} ('at','-l');
						die "$0: exec ('/usr/bin/at','-l') failed, $!, child aborting.\n";
					#else #I'm parent
					};
					while(<GET_AT_JOBS>){
						chomp;
						# the format is somewhat variable, depending
						# upon POSIXLY_CORRECT and possibly other
						# factors
						# [ab=] represent scheduled at (a), batch (b),
						# and running (=) at/batch jobs
						if(s/^(\S+)\s+.* [ab=] \Q$login\E$/\1/){
							push @user_at_jobs,$_;
						};
					};
					unless(close(GET_AT_JOBS)){
						warn "$0: close(GET_AT_JOBS) failed.\n";
						return undef;
					};
					$verbose>=6 && print ("$0: sub ",'$get_at_jobs: @user_at_jobs=(',join(',',@user_at_jobs),")\n");
					return \@user_at_jobs;
				};
				$remove_at_job=sub {
					$verbose>=6 && print ($0,': sub $remove_at_job(',join(',',@_),")\n");
					my $ret=system {'/usr/bin/atrm'} ('atrm',$_[0]);
					if($ret){
						warn ("$0: system ('/usr/bin/atrm',$_[0]) failed",($ret==-1?(": $!"):(", returned: " . ($ret>>8))),"\n");
						$error=1;
						return undef;
					};
					return 1;
				};
			}elsif(
					m!^SuSE SLES-8 \((i386|S/390)\)\nVERSION = 8\.1$!
			){
				&suse_8_1_nscd_stop; #stop the SuSE 8.1 nscd daemon
				$passwd_lock='/etc/passwd.edit';
				$passwd='/etc/passwd';
				$shadow_lock='/etc/shadow.edit';
				$shadow='/etc/shadow';
				$shadow_or_tcb='shadow';
				$unmatchable_pwhash='!';
				$cron_allow='/var/spool/cron/allow';
				$cron_deny='/var/spool/cron/deny';
				$at_allow='/etc/at.allow';
				$at_deny='/etc/at.deny';
				$ftpusers='/etc/ftpusers';
				$list_crontab_needs_allow=1;
				$list_crontab_args=sub {
					return('/usr/bin/crontab','-u',$_[0],'-l');
				};
				$remove_crontab_needs_allow=$list_crontab_needs_allow;
				$remove_crontab_args=sub {
					return('/usr/bin/crontab','-u',$_[0],'-r');
				};
				$restore_crontab_args=sub {
					return('/usr/bin/crontab','-');
				};
				$crontabs_dir='/var/spool/cron/tabs';
				$cron_vixie_cruft=1;
				$get_at_jobs=sub {
					$verbose>=6 && print ($0,': sub $get_at_jobs(',join(',',@_),")\n");
					unless(length($_[0])){
						return undef;
					};
					my $login=$_[0];
					my @user_at_jobs=();

					my $chpid=open(GET_AT_JOBS, '-|');
					if(!defined($chpid)){
						#(implicit) fork failed
						warn "$0: open(GET_AT_JOBS, '-|') failed, $!.\n";
						return undef;
					}elsif(!$chpid){
						#I'm child
						exec {'/usr/bin/at'} ('at','-l');
						die "$0: exec ('/usr/bin/at','-l') failed, $!, child aborting.\n";
					#else #I'm parent
					};
					while(<GET_AT_JOBS>){
						chomp;
						# the format is somewhat variable, depending
						# upon POSIXLY_CORRECT and possibly other
						# factors
						# [ab=] represent scheduled at (a), batch (b),
						# and running (=) at/batch jobs
						if(s/^(\S+)\s+.* [ab=] \Q$login\E$/\1/){
							push @user_at_jobs,$_;
						};
					};
					unless(close(GET_AT_JOBS)){
						warn "$0: close(GET_AT_JOBS) failed.\n";
						return undef;
					};
					$verbose>=6 && print ("$0: sub ",'$get_at_jobs: @user_at_jobs=(',join(',',@user_at_jobs),")\n");
					return \@user_at_jobs;
				};
				$remove_at_job=sub {
					$verbose>=6 && print ($0,': sub $remove_at_job(',join(',',@_),")\n");
					my $ret=system {'/usr/bin/atrm'} ('atrm',$_[0]);
					if($ret){
						warn ("$0: system ('/usr/bin/atrm',$_[0]) failed",($ret==-1?(": $!"):(", returned: " . ($ret>>8))),"\n");
						$error=1;
						return undef;
					};
					return 1;
				};
			};
		}elsif(-r '/etc/debian_version'){
			if(
				-x '/usr/bin/at' &&
				-x '/usr/bin/crontab' &&
				open(DEBIAN_VERSION,,'/etc/debian_version')
			){
				{
					local $/=undef;	# quite temporarily use slurp mode
					$_=<DEBIAN_VERSION>;	# slurp
					$/='';	# quite temporarily use paragraph mode
					chomp;
				};
				unless(close(DEBIAN_VERSION)){
					die "$0: failed to close /etc/debian_version, aborting";
				};
				if(/^3\.[01]$/){
					$passwd_lock='/etc/passwd.edit';
					$passwd='/etc/passwd';
					$shadow_lock='/etc/shadow.edit';
					$shadow='/etc/shadow';
					$shadow_or_tcb='shadow';
					$unmatchable_pwhash='!';
					$cron_allow='/etc/cron.allow';
					$cron_deny='/etc/cron.deny';
					$at_allow='/etc/at.allow';
					$at_deny='/etc/at.deny';
					$ftpusers='/etc/ftpusers';
					$list_crontab_needs_allow=1;
					$list_crontab_args=sub {
						return('/usr/bin/crontab','-u',$_[0],'-l');
					};
					$remove_crontab_needs_allow=$list_crontab_needs_allow;
					$remove_crontab_args=sub {
						return('/usr/bin/crontab','-u',$_[0],'-r');
					};
					$restore_crontab_args=sub {
						return('/usr/bin/crontab','-');
					};
					$crontabs_dir='/var/spool/cron/crontabs';
					$cron_vixie_cruft=0;
					$get_at_jobs=sub {
						$verbose>=6 && print ($0,': sub $get_at_jobs(',join(',',@_),")\n");
						unless(length($_[0])){
							return undef;
						};
						my $login=$_[0];
						my @user_at_jobs=();

						my $chpid=open(GET_AT_JOBS, '-|');
						if(!defined($chpid)){
							#(implicit) fork failed
							warn "$0: open(GET_AT_JOBS, '-|') failed, $!.\n";
							return undef;
						}elsif(!$chpid){
							#I'm child
							exec {'/usr/bin/at'} ('at','-l');
							die "$0: exec ('/usr/bin/at','-l') failed, $!, child aborting.\n";
						#else #I'm parent
						};
						while(<GET_AT_JOBS>){
							chomp;
							# the format is somewhat variable,
							# depending upon POSIXLY_CORRECT and
							# possibly other factors
							# [ab=] represent scheduled at (a),
							# batch (b), and running (=) at/batch jobs
							if(s/^(\S+)\s+.* [ab=] \Q$login\E$/\1/){
								push @user_at_jobs,$_;
							};
						};
						unless(close(GET_AT_JOBS)){
							warn "$0: close(GET_AT_JOBS) failed.\n";
							return undef;
						};
						$verbose>=6 && print ("$0: sub ",'$get_at_jobs: @user_at_jobs=(',join(',',@user_at_jobs),")\n");
						return \@user_at_jobs;
					};
					$remove_at_job=sub {
						$verbose>=6 && print ($0,': sub $remove_at_job(',join(',',@_),")\n");
						my $ret=system {'/usr/bin/atrm'} ('atrm',$_[0]);
						if($ret){
							warn ("$0: system ('/usr/bin/atrm',$_[0]) failed",($ret==-1?(": $!"):(", returned: " . ($ret>>8))),"\n");
							$error=1;
							return undef;
						};
						return 1;
					};
				}else{
					die "$0: don't know how to handle this (apparently Debian or Debian based?) flavor or installation of Linux, aborting.\n";
				};
			}else{
				die "$0: don't know how to handle this (apparently Debian or Debian based?) flavor or installation of Linux, aborting.\n";
			};
		}else{
			die "$0: don't know how to handle this flavor of Linux, aborting.\n";
		};
	}elsif($uname_sr =~ /^HP-UX\b/){
		if($uname_sr =~ /^HP-UX\s+B\.11\.11$/){
			$passwd_lock='/etc/ptmp';
			$passwd='/etc/passwd';
			$shadow_or_tcb='tcb';
			$unmatchable_pwhash='*';
			$cron_allow='/var/adm/cron/cron.allow';
			$cron_deny='/var/adm/cron/cron.deny';
			$at_allow='/var/adm/cron/at.allow';
			$at_deny='/var/adm/cron/at.deny';
			$ftpusers='/etc/ftpd/ftpusers';
			$list_crontab_needs_allow=0;
			$list_crontab_args=sub {
				return('/usr/bin/crontab','-l',$_[0]);
			};
			$remove_crontab_needs_allow=$list_crontab_needs_allow;
			$remove_crontab_args=sub {
				return('/usr/bin/crontab','-r',$_[0]);
			};
			$restore_crontab_args=sub {
				return('/usr/bin/crontab');
			};
			$crontabs_dir='/var/spool/cron/crontabs';
			$cron_vixie_cruft=0;
			$get_at_jobs=sub {
				$verbose>=6 && print ($0,': sub $get_at_jobs(',join(',',@_),")\n");
				unless(length($_[0])){
					return undef;
				};
				my $login=$_[0];
				my @user_at_jobs=();

				my $chpid=open(GET_AT_JOBS, '-|');
				if(!defined($chpid)){
					#(implicit) fork failed
					warn "$0: open(GET_AT_JOBS, '-|') failed, $!.\n";
					return undef;
				}elsif(!$chpid){
					#I'm child
					exec {'/usr/bin/at'} ('at','-l');
					die "$0: exec ('/usr/bin/at','-l') failed, $!, child aborting.\n";
				#else #I'm parent
				};
				while(<GET_AT_JOBS>){
					chomp;
					if(s/^user = \Q$login\E\s+(\S+)\s+\w+\s+\w+\s+\d?\d\s+\d?\d:\d\d:\d\d\s+\d{4}$/\1/){
						push @user_at_jobs,$_;
					};
				};
				unless(close(GET_AT_JOBS)){
					warn "$0: close(GET_AT_JOBS) failed.\n";
					return undef;
				};
				$verbose>=6 && print ("$0: sub ",'$get_at_jobs: @user_at_jobs=(',join(',',@user_at_jobs),")\n");
				return \@user_at_jobs;
			};
			$remove_at_job=sub {
				$verbose>=6 && print ($0,': sub $remove_at_job(',join(',',@_),")\n");
				my $ret=system {'/usr/bin/at'} ('at','-r',$_[0]);
				if($ret){
					warn ("$0: system ('/usr/bin/at','-r',$_[0]) failed",($ret==-1?(": $!"):(", returned: " . ($ret>>8))),"\n");
					$error=1;
					return undef;
				};
				return 1;
			};
		}else{
			die "$0: don't know how to handle this version ($uname_sr) of HP-UX, aborting.\n";
		};
	}elsif($uname_sr =~ /^SunOS\b/){
		if($uname_sr =~ /^SunOS\s+5\.[68]$/){
			&osparams_sunos_5__6_8_10_common;
			$ftpusers='/etc/ftpusers';
		}elsif($uname_sr =~ /^SunOS\s+5\.10$/){
			&osparams_sunos_5__6_8_10_common;
			$ftpusers='/etc/ftpd/ftpusers';
		}else{
			die "$0: don't know how to handle this version ($uname_sr) of SunOS, aborting.\n";
		};
	}else{
		die "$0: don't know how to handle this operating system ($uname_sr), aborting.\n";
	};
};

sub login_name_ok{
	$verbose>=6 && print ($0,': sub login_name_ok(',join(',',@_),")\n");
	my $login=$_[0];
	# we'll presume it's okay, unless we determine otherwise
	if(!$force){
		# more general (and backwards compatible) login name sanity check,
		if($login !~ /^[a-z][a-z0-9]{2,7}$/o){
			# alternative login name restrictions could be based upon ...
			# 3.276 Portable Filename Character Set
			# The set of characters from which portable filenames are
			# constructed.
			# A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
			# a b c d e f g h i j k l m n o p q r s t u v w x y z
			# 0 1 2 3 4 5 6 7 8 9 . _ -
			warn ("$0: login name ($login) failed to match ",'/^[a-z][a-z0-9]{2,7}$/',"\n");
			return undef;
		};
	}else{
			# --force
			# minimal sanity checks;
			# note also that other sanity checks may come into play
			# later if --save is used or password is to be made
			# unmatchable in TCB database
		if	($login =~
				m!
					^(
						root	# disallow root
						|
						# disallow empty
					)$
					|
					:	# disallow colon
					|
					\n	# disallow newline
					|
					\0	# disallow null
				!ox
		){
			warn("$0: login name ($login) cannot be root, empty string, or contain colon (:), newline or null character.\n");
			return undef;
		};
	};
	# additional checks that may be applicable:
	if(defined($save_directory)){
		# since we save in directory with same name as login name, a few
		# more checks are applicable
		if($login =~ m!/|^\.\.?$!o){
			# name of . or .. or containing / is problematic for
			# directory name
			warn "$0: login name ($login) cannot contain slash (/) or be \".\" or \"..\".\n";
			return undef;
		};
	};
	if(($shadow_or_tcb eq 'tcb') && ($login =~ m!/|^\.|-t$!o)){
		# since login name is used for tcb file name and first character
		# of login name is used for tcb file directory name and lockfile
		# appends -t, a few more checks are applicable
			warn "$0: login name ($login) cannot contain slash (/), start with \".\" or end with \"-t\".\n";
			return undef;
	};
	return 1; #we passed all the checks okay
};

sub uid_ok{
	$verbose>=6 && print ($0,': sub uid_ok(',join(',',@_),")\n");
	my $uid=$_[0];
	# sanity check uid
	if	(
			!defined($uid)
			||
			$uid<$minUID
			&&
			!$force
			||
			$uid==0
	){
		return undef;
	}else{
		return 1;
	};
};

sub savepwhash{
	$verbose>=6 && print ($0,': sub savepwhash(',join(',',@_),")\n");
	unless(	sysopen	(	PWHASH,
						$_[1],
						O_WRONLY|
						O_CREAT|
						O_EXCL
					)
	){
		warn(	"$0: failed to open $_[1].\n");
		return undef;
	};
	unless(print PWHASH $_[0]){
		warn(	"$0: failed to write $_[1].\n");
		return undef;
	};
	if(close(PWHASH)){
		return 1;
	};
	warn(	"$0: failed to close $_[1].\n");
	return undef;
};

sub setpwhash{
	$verbose>=6 && print ($0,': sub setpwhash(',join(',',@_),")\n");
	my($login,$pwhash)=@_;

	# sanity check login
	unless(&login_name_ok($login)){
		return undef;
	};
	# sanity check $pwhash
	if(!defined($pwhash) || ($pwhash =~ m':|\n|\0')){
		warn "$0: password hash ($pwhash) must be defined and cannot have colon (:), newline, or nulls in it\n";
		return undef;
	};
	unless(
		($pwhash =~ m'.') ||
		$allowemptypasswordhash
	){
		warn "$0: we don't allow setting of empty password hash\n";
		return undef;
	};
	if	($shadow_or_tcb eq 'shadow'){
		# shadow method

		# obtain our shadow_lock
		my $s=$max_lock_wait_seconds;
		until(sysopen(SHADOW_LOCK,$shadow_lock,O_WRONLY|O_CREAT|O_EXCL)||!$s--){
			sleep(1);
		};
		if($s<=0){
			# this is indicative of having given up waiting for our lock
			warn "$0: failed to obtain lock on $shadow_lock.\n";
			return undef;
		};

		# open shadow file
		unless(open(SHADOW,,$shadow)){
			unlink($shadow_lock);
			close(SHADOW_LOCK);
			warn "$0: failed to open $shadow.\n";
			return undef;
		};

		# shovel data from shadow to shadow_lock, changing password
		# for $login
		while(local $_=<SHADOW>){
			$verbose>=7 && print "$0: sub setpwhash: \$_=$_";
			s"^\Q$login\E:([^:]*):"$login:$pwhash:" &&
			$verbose>=6 && print "$0: sub setpwhash: $login $1 --> $pwhash\n";
			$verbose>=7 && print "$0: sub setpwhash: \$_=$_";
			unless(print SHADOW_LOCK $_){
				unlink($shadow_lock);
				close(SHADOW_LOCK);
				close(SHADOW);
				warn "$0: write failure on $shadow_lock.\n";
				return undef;
			};
		};

		unless(close(SHADOW)){
			unlink($shadow_lock);
			close(SHADOW_LOCK);
			warn "$0: error closing $shadow.\n";
			return undef;
		};

		unless(close(SHADOW_LOCK)){
			unlink($shadow_lock);
			warn "$0: error closing $shadow_lock.\n";
			return undef;
		};

		my @stat;
		if(
			(@stat=stat($shadow))&&
			chown($stat[4],$stat[5],$shadow_lock)&&
			chmod($stat[2],$shadow_lock)
		){
			# seems to return true (1) when successful but don't
			# have an easy way to confirm success
			unless(rename($shadow_lock,$shadow)){
				unlink($shadow_lock);
				warn "$0: rename($shadow_lock,$shadow) failed.\n";
				return undef;
			};
			# so far so good ...
		}else{
			unlink($shadow_lock);
			warn "$0: failed to properly create $shadow_lock.\n";
			return undef;
		};
		# so far so good ...
	}elsif($shadow_or_tcb eq 'tcb'){
		# tcb method
		# except for differences in file format and substitution pattern
		# and lock convention, very similar to shadow processing

		my $tcb='/tcb/files/auth/' . substr($login,0,1) . '/' . $login;
		my $tcb_lock=$tcb . '-t'; # TCB file locking convention

		# obtain our tcb_lock
		my $s=$max_lock_wait_seconds;
		until(sysopen(TCB_LOCK,$tcb_lock,O_WRONLY|O_CREAT|O_EXCL)||!$s--){
			sleep(1);
		};
		if($s<=0){
			# this is indicative of having given up waiting for our lock
			warn "$0: failed to obtain lock on $tcb_lock.\n";
			return undef;
		};

		# open tcb file
		unless(open(TCB,,$tcb)){
			unlink($tcb_lock);
			close(TCB_LOCK);
			warn "$0: failed to open $tcb.\n";
			return undef;
		};

		# shovel data from tcb to tcb_lock, changing password
		# for $login (and failsafing that we set password)
		{
			local $/=undef;	# quite temporarily use slurp mode
			$_=<TCB>;	# slurp
		};
		unless(close(TCB) && defined($_)){
			unlink($tcb_lock);
			close(TCB_LOCK);
			warn "$0: failed to read and close $tcb.\n";
			return undef;
		};
		unless(
			s":u_pwd=[^:]*":u_pwd=$pwhash"go ||
			# failsafe - if u_pwd field wasn't found and we've
			# got chkent field (which marks end of data fields),
			# then we add a u_pwd field with unmatchable password
			s":chkent:":u_pwd=$pwhash:chkent:"g
		){
			unlink($tcb_lock);
			close(TCB_LOCK);
			warn "$0: failed to find u_pwd or chkent field in $tcb.\n";
			return undef;
		};

		unless(print TCB_LOCK $_){
			unlink($tcb_lock);
			close(TCB_LOCK);
			warn "$0: write failure on $tcb_lock.\n";
			return undef;
		};

		unless(close(TCB_LOCK)){
			unlink($tcb_lock);
			warn "$0: close failure on $tcb_lock.\n";
			return undef;
		};

		my @stat;
		if(
			(@stat=stat($tcb))&&
			chown($stat[4],$stat[5],$tcb_lock)&&
			chmod($stat[2],$tcb_lock)
		){
			# seems to return true (1) when successful but don't
			# have an easy way to confirm success
			unless(rename($tcb_lock,$tcb)){
				unlink($tcb_lock);
				warn "$0: rename($tcb_lock,$tcb) failed.\n";
				return undef;
			};
			# so far so good ...
		}else{
			unlink($tcb_lock);
			warn "$0: failed to properly create $tcb_lock.\n";
			return undef;
		};
		# so far so good ...
	}else{
		warn "$0: don't know how to set password.\n";
		return undef;
	};

	# so far so good ...
	# now we want to confirm we actually made the password unmatchable
	# (e.g. in case an authentication method other than files is used
	# for $login and we failed to make the password for $login unmatchable)
	if(((getpwnam($login))[1]) eq $pwhash){
		# appears everything went fine
		return 1;
	}else{
		warn "$0: don't know how to set password for $login.\n";
		return undef;
	};
};

sub restorepwhash{
	$verbose>=6 && print ($0,': sub restorepwhash(',join(',',@_),")\n");
	unless(
		open(PWHASH,,$_[1])
	){
		warn "$0: failed to open $_[1]\n";
		return undef;
	};
	local $_;
	{
		local $/=undef;	# quite temporarily use slurp mode
		$_=<PWHASH>;	# slurp
		$/='';	# quite temporarily use paragraph mode
		chomp;
	};
	if(!defined($_)){
		# empty file would trigger this,
		# empty file indicative of empty pwhash field,
		# so we want this to properly be a string
		$_='';
	};
	unless(close(PWHASH)){
		warn "$0: failed to close $_[1]\n";
		return undef;
	};
	return(&setpwhash($_[0],$_));
};

# get administrative lock status
# return true if not locked
# return false but defined if locked
# return undef otherwise
sub is_not_alocked{
	$verbose>=6 && print ($0,': sub is_not_alocked(',join(',',@_),")\n");
	unless(length($_[0])){
		return undef;
	};

	#to ensure the login name is a single argument, we do this via exec
	#$_=`/usr/lbin/getprpw -m alock -- $_[0]`;
	my $chpid=open(GETPRPW, '-|');
	if(!defined($chpid)){
		#(implicit) fork failed
		warn "$0: open(GETPRPW, '-|') failed, $!.\n";
		return undef;
	}elsif(!$chpid){
		#I'm child
		exec {'/usr/lbin/getprpw'} ('getprpw','-m','alock','--',$_[0]);
		die "$0: exec ('/usr/lbin/getprpw','-m','alock','--','$_[0]') failed, $!, child aborting.\n";
	#else #I'm parent
	};
	{
		local $/=undef;	# quite temporarily use slurp mode
		$_=<GETPRPW>;	# slurp
		$/='';	# quite temporarily use paragraph mode
		chomp;
	};
	unless(close(GETPRPW)){
		warn "$0: close(GETPRPW) failed.\n";
		return undef;
	};

	if('alock=NO' eq $_){
		return 1;
	}elsif('alock=YES' eq $_){
		return 0;
	};
	warn "$0: failed to determine alock status for login $_[0] via exec {'/usr/lbin/getprpw'} ('getprpw','-m','alock','--','$_[0]').\n";
	return undef;
};

sub mkfile{
	$verbose>=6 && print ($0,': sub mkfile(',join(',',@_),")\n");
	unless(length($_[0])){
		return undef;
	};
	unless(	sysopen	(	FILE,
						$_[0],
						O_WRONLY|O_CREAT|O_EXCL
					)
	){
		warn "$0: failed to open $_[0].\n";
		return undef;
	};
	unless(close(FILE)){
		warn "$0: failed to close $_[0].\n";
		return undef;
	};
	return 1;
};

# if administrative lock is not set, we save that state information and return true
# if administrative lock is set, we return false but defined
# if something goes wrong (can't determine or save state) we return undef
sub savealock{
	$verbose>=6 && print ($0,': sub savealock(',join(',',@_),")\n");
	unless(length($_[0]) && length($_[1])){
		return undef;
	};
	my $is_not_alocked=&is_not_alocked($_[0]);
	if(!defined($is_not_alocked)){
		warn "$0: failed to determine alock status for login $_[0].";
		return undef;
	}elsif($is_not_alocked){
		return mkfile($_[1]);
	}else{
		return 0;
	};
};

sub setalock{
	$verbose>=6 && print ($0,': sub setalock(',join(',',@_),")\n");
	unless(&login_name_ok($_[0])){
		return undef;
	};
	my $lockarg='alock=YES';
	if(
		defined($_[1]) &&
		$_[1] =~ m'^(alock=|)no\n*$'i
	){
		$lockarg='alock=NO';
	};
	$verbose>=7 && print "$0: sub setalock \$lockarg=$lockarg\n";
	my $ret=system {'/usr/lbin/modprpw'} ('modprpw','-m',$lockarg,'--',$_[0]);
	if($ret){
		warn ("$0: system {'/usr/lbin/modprpw'} ('modprpw','-m',$lockarg,'--','$_[0]') failed",($ret==-1?(": $!"):(", returned: " . ($ret>>8))),"\n");
		return undef;
	};
	#else:
	return 1;
};

sub savehome{
	$verbose>=6 && print ($0,': sub savehome(',join(',',@_),")\n");
	unless(defined($_[0]) && length($_[1])){
		return undef;
	};
	unless( sysopen	(	HOME,
						$_[1],
						O_WRONLY|O_CREAT|O_EXCL
					)
	){
		warn "$0: failed to open $_[1].\n";
		return undef;
	};
	unless(print HOME $_[0]){
		warn "$0: failed to write $_[1].\n";
		return undef;
	};
	unless(close(HOME)){
		warn "$0: failed to close $_[1].\n";
		return undef;
	};
	return 1;
};

sub sethome{
	$verbose>=6 && print ($0,': sub sethome(',join(',',@_),")\n");
	my($login,$home)=@_;

	# sanity check login
	unless(&login_name_ok($login)){
		return undef;
	};

	# sanity check home
	if(!defined($home) || ($home =~ m':|\n|\0')){
		warn "$0: HOME ($home) must be defined and cannot have colon (:), newline, or nulls in it.\n";
		return undef;
	};

	# obtain our passwd lock
	my $s=$max_lock_wait_seconds;
	until(sysopen(PASSWD_LOCK,$passwd_lock,O_WRONLY|O_CREAT|O_EXCL)||!$s--){
		sleep(1);
	};
	if($s<=0){
		# this is indicative of having given up waiting for our lock
		warn "$0: failed to obtain lock on $passwd_lock.\n";
		return undef;
	};

	# open passwd file
	unless(open(PASSWD,,$passwd)){
		unlink($passwd_lock);
		close(PASSWD_LOCK);
		warn "$0: failed to open $passwd.\n";
		return undef;
	};

	# shovel data from passwd to passwd lock, changing HOME
	# for $login
	while(local $_=<PASSWD>){
		$verbose>=7 && print "$0: sub sethome: \$_=$_";
		if(my($x,$y,$z)=m"^(\Q$login\E:.*:)([^:]*)(:[^:]*)$"){
			unless($y eq $home){
				# not already changed
				$_=$x . $home . $z;	# reassemble the whole line
				$verbose>=6 && print "$0: sub sethome: $login $y --> $home\n";
			};
			$verbose>=7 && print "$0: sub sethome: \$_=$_";
		};
		unless(print PASSWD_LOCK $_){
			unlink($passwd_lock);
			close(PASSWD_LOCK);
			warn "$0: write failure on $passwd_lock.\n";
			return undef;
		};
	};

	unless(close(PASSWD)){
		unlink($passwd_lock);
		close(PASSWD_LOCK);
		warn "$0: error closing $passwd.\n";
		return undef;
	};

	unless(close(PASSWD_LOCK)){
		unlink($passwd_lock);
		warn "$0: close failure on $passwd_lock.\n";
		return undef;
	};

	my @stat;
	if(
		(@stat=stat($passwd))&&
		chown($stat[4],$stat[5],$passwd_lock)&&
		chmod($stat[2],$passwd_lock)
	){
		# seems to return true (1) when successful but don't
		# have an easy way to confirm success
		unless(rename($passwd_lock,$passwd)){
			unlink($passwd_lock);
			warn "$0: rename($passwd_lock,$passwd) failed.\n";
			return undef;
		};
	}else{
		unlink($passwd_lock);
		warn "$0: failed to properly create $passwd_lock.\n";
		return undef;
	};

	# now we want to confirm we actually set HOME
	# (e.g. in case an authentication method other than files is used
	# for $login and we failed to set the HOME for $login
	if(
		((getpwnam($login))[7])
		eq
		$home
	){
		# appears everything went fine
		return 1;
	}else{
		warn "$0: don't know how to set HOME.\n";
		return undef;
	};
};

sub lockhome{
	$verbose>=6 && print ($0,': sub lockhome(',join(',',@_),")\n");
	my $login=$_[0];

	# sanity check login
	unless(&login_name_ok($login)){
		return undef;
	};

	# we use local $_ to construct what will be HOME
	local $_=((getpwnam($login))[7]);

	unless(defined($_)){
		warn "$0: failed to determine current HOME for $login\n";
		return undef;
	};

	if(m"^\Q$locked_home_prefix\E(/|$)"){
		# already locked
		return 1;
	};

	$_=$locked_home_prefix . '/' . $_;	# prepend
	s'/{2,}'/'og;		# multiple consecutive /'s to single /
	s'([^/])/+$'$1';	# strip trailing /'s, but not leading /
	return(&sethome($login,$_));
};

sub restorehome{
	$verbose>=6 && print ($0,': sub restorehome(',join(',',@_),")\n");
	unless(
		open(HOME,,$_[1])
	){
		warn "$0: failed to open $_[1]\n";
		return undef;
	};
	local $_;
	{
		local $/=undef;	# quite temporarily use slurp mode
		$_=<HOME>;	# slurp
		$/='';	# quite temporarily use paragraph mode
		chomp;
	};
	if(!defined($_)){
		# empty file would trigger this,
		# empty file indicative of empty home field,
		# so we want this to properly be a string
		$_='';
	};
	unless(close(HOME)){
		warn "$0: failed to close $_[1]\n";
		return undef;
	};
	return(&sethome($_[0],$_));
};

# something like fgrep -qx, but only check a single fixed string
# (no newlines).
# return true for matched,
# defined but false for not matched,
# undef in case of problem(s)
sub fgrep{
	$verbose>=6 && print ($0,': sub fgrep(',join(',',@_),")\n");
	unless(defined($_[0]) && length($_[1])){
		return undef;
	};
	my ($string,$file)=@_[0..1];

	# strip leading and trailing newline(s) from our string
	$string =~ s/^\n+//o;
	$string =~ s/\n+$//o;

	# we can't have a newline in our string
	($string !~ /\n/o) or return undef;

	unless(open(FILE,,$file)){
		warn "$0: open on $file failed.\n";
		return undef;
	};

	while(<FILE>){
		if(/^\Q$string\E$/){
			unless(close(FILE)){
				warn "$0: error closing $file.\n";
				return 1; # we still found it okay
			};
			# already there
			return 1;
		};
	};

	# got to the end and didn't find it
	unless(close(FILE)){
		warn "$0: error closing $file.\n";
		return undef;
	};
	return 0;
};

########################################################################
# add_line_to_file_if_not_there
# given a (possibly empty) line (string) and a file (pathname),
# we add the provided line to the file, if it's not already in that file
# return:
#	true if we added line to the file
#	false but defined if string was already in file
# 	undef if something went wrong
# note that we may also create file if it doesn't already exist
########################################################################
sub add_line_to_file_if_not_there{
	$verbose>=6 && print ($0,': sub add_line_to_file_if_not_there(',join(',',@_),")\n");
	unless(defined($_[0]) && length($_[1])){
		return undef;
	};
	my ($string,$file)=@_;

	# strip leading and trailing newline(s) from our string
	$string =~ s/^\n+//o;
	$string =~ s/\n+$//o;

	# we can't have a newline in our string
	($string !~ /\n/o) or return undef;

	unless(sysopen(FILE_TO_ADD_LINE_TO_IF_NOT_THERE,$file,O_RDWR|O_CREAT)){
		warn "$0: sysopen on $file failed.\n";
		return undef;
	};

	# keep track if last character we read was a newline
	my $nl;
	# keep track if we read something from file (non-zero length)
	my $nz;

	while(<FILE_TO_ADD_LINE_TO_IF_NOT_THERE>){
		$nl=chomp;
		$nz=1;
		if(/^\Q$string\E$/){
			# already there
			unless($nl){
				# but missing trailing newline at end of file, add it
				unless(print FILE_TO_ADD_LINE_TO_IF_NOT_THERE "\n"){
					warn "$0: error writing $file.\n";
					close(FILE_TO_ADD_LINE_TO_IF_NOT_THERE);
					return undef;
				};
				unless(close(FILE_TO_ADD_LINE_TO_IF_NOT_THERE)){
					warn "$0: error closing $file.\n";
					return undef;
				};
				# we'll claim we added it, since it was missing the
				# trailing newline and we added that
				return 1;
			};
			unless(close(FILE_TO_ADD_LINE_TO_IF_NOT_THERE)){
				warn "$0: error closing $file.\n";
				return undef;
			};
			return 0; # found it, didn't write file
		};
	};

	# got to the end and didn't find it, we need to add it
	unless	(print FILE_TO_ADD_LINE_TO_IF_NOT_THERE
				(
					($nz && ! $nl)
						?
							# non-zero length input but no ending newline
							# so we need to also prepend a newline
							"\n$string\n"
							# We could treat non-zero with missing trailing
							# newline as some kind of error or exceptional
							# case, but we'll presume the item before the
							# missing newline was intended to be
							# effective, and we'll quietly add that
							# newline, and then our string (and of course it's
							# newline).
						:
							# zero length input or already got an ending newline
							# so just add our string and newline
							"$string\n"
				)
			){
		warn "$0: error writing $file.\n";
		close(FILE_TO_ADD_LINE_TO_IF_NOT_THERE);
		return undef; # our attempt to add it didn't work
	};
	unless(close(FILE_TO_ADD_LINE_TO_IF_NOT_THERE)){
		warn "$0: error closing $file.\n";
		return undef;
	};

	return 1; #added it fine
};

########################################################################
# remove_line_from_file_if_there
# given a (possibly empty) line (string) and a file (pathname),
# we remove the provided line from the file, if it's already in that file
# return:
#	true if we removed line from the file
#	false but defined if string wasn't in file to start with
# 	undef if something went wrong
# note that we may also create file if it doesn't already exist
########################################################################
sub remove_line_from_file_if_there{
	$verbose>=6 && print ($0,': sub remove_line_from_file_if_there(',join(',',@_),")\n");
	unless(defined($_[0]) && length($_[1])){
		return undef;
	};
	my ($string,$file)=@_;
	local $_;

	# strip leading and trailing newline(s) from our string
	$string =~ s/^\n+//o;
	$string =~ s/\n+$//o;

	# we can't have a newline in our string
	($string !~ /\n/o) or return undef;

	unless(sysopen(FILE_TO_REMOVE_LINE_FROM_IF_THERE,$file,O_RDWR|O_CREAT)){
		warn "$0: sysopen on $file failed.\n";
		return undef;
	};

	{
		local $/=undef;	# quite temporarily use slurp mode
		$_=<FILE_TO_REMOVE_LINE_FROM_IF_THERE>;	# slurp
	};

	my $found=undef;	# track if we found our string

	if($string ne ''){
		# non-empty string
		# is the string already there properly or missing trailing newline?
		# may need to do this mutlipe times due to possible RE overlap
		while(
			s/(^|\n)\Q$string\E(?:\n|$)/\1/g
		){
			$found=1;
		};
	}else{
		# empty string is special case
		$found=s/\n{2,}/\n/go;		# colapse multiple consecutive newlines to a single newline
		s/^\n+//go and $found=1;	# strip leading newlines
	};
	if($found){
		if(
			seek(FILE_TO_REMOVE_LINE_FROM_IF_THERE,0,0)
			&&
			truncate(FILE_TO_REMOVE_LINE_FROM_IF_THERE,0)
			&&
			(print FILE_TO_REMOVE_LINE_FROM_IF_THERE $_)
		){
			unless(close(FILE_TO_REMOVE_LINE_FROM_IF_THERE)){
				warn "$0: error closing $file.\n";
				return undef;
			};
			return 1;	# found and updated fine
		}else{
			warn "$0: error writing $file.\n";
			close(FILE_TO_REMOVE_LINE_FROM_IF_THERE);
			return undef;
		};
	};
	#else
	return 0;	# not found
};

# get_cron_allow, is login $_[0] allowed cron access?
# return true if allowed
# 		 false but defined if disallowed
# 		 undefined if failed to determine
sub get_cron_allow{
	$verbose>=6 && print ($0,': sub get_cron_allow(',join(',',@_),")\n");
	unless(length($_[0])){
		return undef;
	};
	my $login=$_[0];
	if(-f $cron_allow){
		my $ret=&fgrep($login,$cron_allow);
		if(defined($ret)){
			return($ret);
		}else{
			warn "$0: sub get_cron_allow failed to determine cron allow/deny status for $login\n";
			return(undef);
		};
	}elsif(-f $cron_deny){
		my $ret=&fgrep($login,$cron_deny);
		if(defined($ret)){
			return(!$ret);
		}else{
			warn "$0: sub get_cron_allow failed to determine cron allow/deny status for $login\n";
			return(undef);
		};
	}else{
		warn "$0: sub get_cron_allow failed to determine cron allow/deny status for $login\n";
		return(undef);
	};
};

# allow cron access for login $_[0]
sub set_cron_allow{
	$verbose>=6 && print ($0,': sub set_cron_allow(',join(',',@_),")\n");
	unless(length($_[0])){
		return undef;
	};
	my $login=$_[0];
	if(&get_cron_allow($login)){
		# already allowed
		return 1;
	};
	if(-f $cron_allow){
		if(&add_line_to_file_if_not_there($login,$cron_allow)){
			return 1;
		}else{
			warn "$0: set_cron_allow failed to add $login to $cron_allow\n";
			$error=1;
			return 0;
		};
	}elsif(-f $cron_deny){
		if(&remove_line_from_file_if_there($login,$cron_deny)){
			return 1;
		}else{
			warn "$0: failed to remove $login from $cron_deny\n";
			$error=1;
			return 0;
		};
	}else{
		if(&add_line_to_file_if_not_there($login,$cron_allow)){
			warn "$0: NOTICE: created $cron_allow\n";
			return 1;
		}else{
			warn "$0: failed to add $login to $cron_allow\n";
			$error=1;
			return 0;
		};
	};
};

# deny cron access for login $_[0]
sub set_cron_deny{
	$verbose>=6 && print ($0,': sub set_cron_deny(',join(',',@_),")\n");
	unless(length($_[0])){
		return undef;
	};
	my $login=$_[0];
	my $cron_was_allowed=undef;
	$cron_was_allowed=&get_cron_allow($login);
	if(!$cron_was_allowed && defined($cron_was_allowed)){
		# already denied
		return 1;
	};
	if(-f $cron_allow){
		if(&remove_line_from_file_if_there($login,$cron_allow)){
			return 1;
		}else{
			warn "$0: set_cron_deny failed to remove $login from $cron_allow\n";
			$error=1;
			return 0;
		};
	}elsif(-f $cron_deny){
		if(&add_line_to_file_if_not_there($login,$cron_deny)){
			return 1;
		}else{
			warn "$0: failed to add $login to $cron_deny\n";
			$error=1;
			return 0;
		};
	}else{
		if (&mkfile($cron_allow)){
			warn "$0: NOTICE: created $cron_allow\n";
			return 1;
		}else{
			warn "$0: failed to create $cron_allow\n";
			$error=1;
			return 0;
		};
	};
};

# return crontab of $_[0] (login)
# if no crontab or empty crontab, return empty string,
# if cannot determine crontab or error, return undef
sub getcrontab{
	$verbose>=6 && print ($0,': sub getcrontab(',join(',',@_),")\n");
	unless(length($_[0])){
		return undef;
	};
	my $login=$_[0];

	if(! -s "$crontabs_dir/$login"){
		# crontab file empty or not present
		return '';
	};

	my $cron_was_allowed=undef;
	if($list_crontab_needs_allow){
		$cron_was_allowed=&get_cron_allow($login);
		if(!$cron_was_allowed){
			&set_cron_allow($login);
		};
	};

	my @crontab=();
	my $chpid=open(GET_CRONTAB, '-|');
	if(!defined($chpid)){
		#(implicit) fork failed
		warn "$0: open(GET_CRONTAB, '-|') failed, $!.\n";
		if($list_crontab_needs_allow&&!$cron_was_allowed){
			&set_cron_deny($login);
		};
		return undef;
	}elsif(!$chpid){
		#I'm child
		exec &$list_crontab_args($login);
		die ("$0: exec ('",join("','",&$list_crontab_args($login)),"') failed, $!, child aborting.\n");
	#else #I'm parent
	};
	while(<GET_CRONTAB>){
		push @crontab,$_;
	};
	unless(close(GET_CRONTAB)){
		warn "$0: close(GET_CRONTAB) failed.\n";
		if($list_crontab_needs_allow&&!$cron_was_allowed){
			&set_cron_deny($login);
		};
		return undef;
	};
	if($cron_vixie_cruft){
		if($crontab[0] =~ /^# DO NOT EDIT THIS FILE.*edit the master/i){
			shift @crontab;
			if($crontab[0] =~ /^# .*installed on/i){
				shift @crontab;
				if($crontab[0] =~ /^# .*Cron version.* vixie/i){
					shift @crontab;
				};
			};
		};
	};

	if($list_crontab_needs_allow&&!$cron_was_allowed){
		&set_cron_deny($login);
	};

	return(join('',@crontab));
};

sub savecrontab{
	$verbose>=6 && print ($0,': sub savecrontab(',join(',',@_),")\n");
	unless(length($_[0]) && length($_[1])){
		return undef;
	};
	unless( sysopen	(	SAVECRONTAB,
						$_[1],
						O_WRONLY|O_CREAT|O_EXCL
					)
	){
		warn "$0: failed to open $_[1].\n";
		return undef;
	};
	unless(print SAVECRONTAB "$_[0]"){
		warn "$0: failed to write $_[1].\n";
		return undef;
	};
	unless(close(SAVECRONTAB)){
		warn "$0: failed to close $_[1].\n";
		return undef;
	};
	return 1;
};

sub saveshell{
	$verbose>=6 && print ($0,': sub saveshell(',join(',',@_),")\n");
	unless(defined($_[0]) && length($_[1])){
		return undef;
	};
	unless( sysopen	(	SHELL,
						$_[1],
						O_WRONLY|O_CREAT|O_EXCL
					)
	){
		warn "$0: failed to open $_[1].\n";
		return undef;
	};
	if(
		length($_[0]) &&
		!(print SHELL "$_[0]")
	){
		warn "$0: failed to write $_[1].\n";
		return undef;
	};
	unless(close(SHELL)){
		warn "$0: failed to close $_[1].\n";
		return undef;
	};
	return 1;
};

sub shellis{
	$verbose>=6 && print ($0,': sub shellis(',join(',',@_),")\n");
	unless(
		length($_[0])&&
		defined($_[1])
	){
		return undef;
	};
	my $shell=((getpwnam($_[0]))[8]);
	if(defined($shell)){
		return($shell eq $_[1]);
	}else{
		return undef;
	};
};

sub setshell{
	$verbose>=6 && print ($0,': sub setshell(',join(',',@_),")\n");
	unless(
		length($_[0])&&
		length($_[1])
	){
		return undef;
	};
	my $login=$_[0];
	my $shell=$_[1];

	# sanity check login
	unless(&login_name_ok($login)){
		return undef;
	};

	# sanity check shell
	if($shell =~ m':|\n|\0'){
		warn "$0: shell ($shell) cannot have colon (:), newline, or nulls in it\n";
		return undef;
	};

	# obtain our passwd lock
	my $s=$max_lock_wait_seconds;
	until(sysopen(PASSWD_LOCK,$passwd_lock,O_WRONLY|O_CREAT|O_EXCL)||!$s--){
		sleep(1);
	};
	if($s<=0){
		# this is indicative of having given up waiting for our lock
		warn "$0: failed to obtain lock on $passwd_lock.\n";
		return undef;
	};

	# open passwd file
	unless(open(PASSWD,,$passwd)){
		unlink($passwd_lock);
		close(PASSWD_LOCK);
		warn "$0: failed to open $passwd.\n";
		return undef;
	};

	# shovel data from passwd to passwd lock, changing shell
	# for $login
	while(local $_=<PASSWD>){
		$verbose>=7 && print "$0: sub setshell: \$_=$_";
		if(
			! m":\Q$shell\E\n$" &&	# shell not already changed
			s"^(\Q$login\E:.*:)([^:]*)$"\1$shell\n" &&	# change shell for $login
			$verbose>=6
		){
			$2 =~ '^([^\n]*)';
			print ("$0: sub setshell: $login $1 --> $shell\n");
		};
		$verbose>=7 && print "$0: sub setshell: \$_=$_";
		unless(print PASSWD_LOCK $_){
			unlink($passwd_lock);
			close(PASSWD_LOCK);
			warn "$0: write failure on $passwd_lock.\n";
			return undef;
		};
	};

	unless(close(PASSWD)){
		unlink($passwd_lock);
		close(PASSWD_LOCK);
		warn "$0: error closing $passwd.\n";
		return undef;
	};

	unless(close(PASSWD_LOCK)){
		unlink($passwd_lock);
		warn "$0: close failure on $passwd_lock.\n";
		return undef;
	};

	my @stat;
	if(
		(@stat=stat($passwd))&&
		chown($stat[4],$stat[5],$passwd_lock)&&
		chmod($stat[2],$passwd_lock)
	){
		# seems to return true (1) when successful but don't
		# have an easy way to confirm success
		unless(rename($passwd_lock,$passwd)){
			unlink($passwd_lock);
			warn "$0: rename($passwd_lock,$passwd) failed.\n";
			return undef;
		};
	}else{
		unlink($passwd_lock);
		warn "$0: failed to properly create $passwd_lock.\n";
		return undef;
	};

	# now we want to confirm we actually set the shell
	# (e.g. in case an authentication method other than files is used
	# for $login and we failed to set the shell for $login
	if(&shellis($login,$shell)){
		# appears everything went fine
		return 1;
	}else{
		warn "$0: don't know how to set shell for $login.\n";
		return undef;
	};
};

# get_at_allow, is login $_[0] allowed at access?
# return true if allowed
# 		 false but defined if disallowed
# 		 undefined if failed to determine
sub get_at_allow{
	$verbose>=6 && print ($0,': sub get_at_allow(',join(',',@_),")\n");
	unless(length($_[0])){
		return undef;
	};
	my $login=$_[0];
	if(-f $at_allow){
		my $ret=&fgrep($login,$at_allow);
		if(defined($ret)){
			return($ret);
		}else{
			warn "$0: sub get_at_allow failed to determine at allow/deny status for $login\n";
			return(undef);
		};
	}elsif(-f $at_deny){
		my $ret=&fgrep($login,$at_deny);
		if(defined($ret)){
			return(!$ret);
		}else{
			warn "$0: sub get_at_allow failed to determine at allow/deny status for $login\n";
			return(undef);
		};
	}else{
		warn "$0: sub get_at_allow failed to determine at allow/deny status for $login\n";
		return(undef);
	};
};

# allow at access for login $_[0]
sub set_at_allow{
	$verbose>=6 && print ($0,': sub set_at_allow(',join(',',@_),")\n");
	unless(length($_[0])){
		return undef;
	};
	my $login=$_[0];
	if(&get_at_allow($login)){
		# already allowed
		return 1;
	};
	if(-f $at_allow){
		if(&add_line_to_file_if_not_there($login,$at_allow)){
			return 1;
		}else{
			warn "$0: set_at_allow failed to add $login to $at_allow\n";
			$error=1;
			return 0;
		};
	}elsif(-f $at_deny){
		if(&remove_line_from_file_if_there($login,$at_deny)){
			return 1;
		}else{
			warn "$0: failed to remove $login from $at_deny\n";
			$error=1;
			return 0;
		};
	}else{
		if(&add_line_to_file_if_not_there($login,$at_allow)){
			warn "$0: NOTICE: created $at_allow\n";
			return 1;
		}else{
			warn "$0: failed to add $login to $at_allow\n";
			$error=1;
			return 0;
		};
	};
};

# deny at access for login $_[0]
sub set_at_deny{
	$verbose>=6 && print ($0,': sub set_at_deny(',join(',',@_),")\n");
	unless(length($_[0])){
		return undef;
	};
	my $login=$_[0];
	my $at_was_allowed=undef;
	$at_was_allowed=&get_at_allow($login);
	if(!$at_was_allowed && defined($at_was_allowed)){
		# already denied
		return 1;
	};
	if(-f $at_allow){
		if(&remove_line_from_file_if_there($login,$at_allow)){
			return 1;
		}else{
			warn "$0: set_at_deny failed to remove $login from $at_allow\n";
			$error=1;
			return 0;
		};
	}elsif(-f $at_deny){
		if(&add_line_to_file_if_not_there($login,$at_deny)){
			return 1;
		}else{
			warn "$0: failed to add $login to $at_deny\n";
			$error=1;
			return 0;
		};
	}else{
		if (&mkfile($at_allow)){
			warn "$0: NOTICE: created $at_allow\n";
			return 1;
		}else{
			warn "$0: failed to create $at_allow\n";
			$error=1;
			return 0;
		};
	};
};

sub restoreshell{
	$verbose>=6 && print ($0,': sub restoreshell(',join(',',@_),")\n");
	unless(
		open(SHELL,,$_[1])
	){
		warn "$0: failed to open $_[1]\n";
		return undef;
	};
	local $_;
	{
		local $/=undef;	# quite temporarily use slurp mode
		$_=<SHELL>;	# slurp
		$/='';	# quite temporarily use paragraph mode
		chomp;
	};
	if(!defined($_)){
		# empty file would trigger this,
		# empty file indicative of empty shell field (default system shell),
		# so we want this to properly be a string
		$_='';
	};
	unless(close(SHELL)){
		warn "$0: failed to close $_[1]\n";
		return undef;
	};
	return(&setshell($_[0],$_));
};

# kill/signal pids belonging to UID,
# first argument is UID,
# additional arguments are optional, and are signals to use, in order,
# defaults to single use of 9 (SIGKILL)
sub kill_uid_signals{
	$verbose>=6 && print ($0,': kill_uid_signals(',join(',',@_),")\n");
	my @signals=@_;
	my $uid=shift(@signals);
	unless(&uid_ok($uid)){
		die "$0: kill_uid_signals: UID $uid fails check, aborting.\n";
	};
	if(!@signals){
		# default case
		@signals=(9);
	};
	# sanity check proposed signal(s)
	for (@signals){
		unless(/^(?:[1-9]\d+|\d)$/ && $_ <= $#signal_names){
			die "$0: kill_uid_signals: bad signal: $_, aborting.\n";
		};
	};
	my $chpid=fork;
	if(!defined($chpid)){
		warn "$0: kill_uid_signals: fork failed!\n";
		return undef;
	}elsif(!$chpid){
		# I'm the child
		($<,$>)=($uid,$uid); # set real and effective UID to $uid
		unless(($<,$>)==($uid,$uid)){
			warn "$0: (\$<,\$>)=($uid,$uid) apparently failed, aborting\n";
			exit 1; # This should be sufficient for parent to know I was bad
		};
		while(){
			if(!defined($_=shift(@signals))){
				exit 0; # no signals left to process
			};
			$SIG{$signal_names[$_]}='IGNORE'; # try suicide prevention
			kill $_, -1; # and then possibly non-fatal suicide attempt
			if(@signals){
				sleep($signal_sleep); # delay slightly before looping
			}else{
				exit 0;
			};
		};
	}else{
		# I'm the parent
		waitpid $chpid,0;
		if($?<0||$#signal_names<$?){
			# child is telling us there was a problem
			warn "$0: kill_uid_signals: our child had a problem, returned: \$?=$?\n";
			return undef;
		}else{
			# death by signal or exit 0
			# not a problem
			return 1;
		};
	};
};

sub putcrontab{
	$verbose>=6 && print ($0,': putcrontab(',join(',',@_),")\n");
	my ($login,$crontab)=@_;

	unless(&login_name_ok($login)){
		return undef;
	};

	my $uid=(getpwnam($login))[2];
	unless(&uid_ok($uid)){
		warn "$0: putcrontab UID $uid=(getpwnam($login))[2] out of range.\n";
		return undef;
	};

	unless(open(CRONTAB,,$crontab)){
		warn "$0: putcrontab: failed to open $crontab: $!\n";
		return undef;
	};
	my $crontabdata='';
	{
		local $/=undef;	# quite temporarily use slurp mode
		$crontabdata=<CRONTAB>;	# slurp
	};
	unless(close(CRONTAB)){
		warn "$0: putcrontab failed to close $crontab: $!";
		return undef;
	};

	unless(&set_cron_allow($login)){
		warn "$0: set_cron_allow failed for login $login\n";
		return undef;
	};
	
	my $chpid=open(PUTCRONTAB,'|-');
	if(!defined($chpid)){
		warn "$0: putcrontab: fork failed!\n";
		return undef;
	}elsif(!$chpid){
		# I'm the child
		($<,$>)=($uid,$uid); # set real and effective UID to $uid
		unless(($<,$>)==($uid,$uid)){
			warn "$0: (\$<,\$>)=($uid,$uid) apparently failed, aborting\n";
			exit 1; # This should be sufficient for parent to know I was bad
		};
		exec &$restore_crontab_args;
		die ("$0: putcrontab: login $login UID $uid exec ('",join("','",&$restore_crontab_args),"') failed, $!, child aborting.\n");
	}else{
		# I'm the parent
		unless(print PUTCRONTAB ($crontabdata)){
			warn "$0: putcrontab: error writing crontab: $!";
			close(PUTCRONTAB);
			return undef;
		};
		unless(close(PUTCRONTAB)){
			warn "$0: putcrontab: error closing crontab: $!";
			return undef;
		};
		# close()ing our pipe waits for the child process and sets $?
		if($?){
			# child is telling us there was a problem
			warn "$0: putcrontab: our child had a problem, returned: \$?=$?\n";
			return undef;
		}else{
			# returned status of 0, all apparently okay:
			return 1;
		};
	};
};

# more variables we'll likely need to use
my %logins_done=();	# logins we've completed processing
my %uids_done=();	# uids we've completed processing
my @pwent=();		# where we store return from getpw*()

# basename portion of filenames we may use in save/restore directory
# Some of these store actual data,
# others are used as booleans indicators by their being created or
# not being created.
# Note that with the "booleans" we generally create them to indicate
# we'd do a more permissive (open/unlock capability) in case of
# --restore, as these would generally be locked down upon disabling.
my $savepwhash='pwhash';
my $savealock='alock=NO';
my $savehome='HOME';
my $saveftpusers='ftpusers';
my $saveshell='shell';
my $savecronallow='cron.allow';
my $saveatallow='at.allow';
my $savecrontab='crontab';

sub save_cron_at_allow{
	$verbose>=6 && print ($0,': sub save_cron_at_allow(',join(',',@_),")\n");
	my ($login,$save_directory_login)=@_;
	# sanity check login
	unless(&login_name_ok($login)){
		return undef;
	};

	my $ret=1;

	if(-f $cron_allow){
		if(&fgrep($login,$cron_allow)){
			&mkfile("$save_directory_login/$savecronallow") or $ret=0;
		};
	}elsif(-f $cron_deny){
		my $ret=&fgrep($login,$cron_deny);
		if(defined($ret)&&!$ret){
			&mkfile("$save_directory_login/$savecronallow") or $ret=0;
		};
	};

	if(-f $at_allow){
		if(&fgrep($login,$at_allow)){
			&mkfile("$save_directory_login/$saveatallow") or $ret=0;
		};
	}elsif(-f $at_deny){
		my $ret=&fgrep($login,$at_deny);
		if(defined($ret)&&!$ret){
			&mkfile("$save_directory_login/$saveatallow") or $ret=0;
		};
	};

	return $ret;
};

# we're almost certainly going to need these operating system specific
# parameters anyway, so might as well grab them now if we don't already
# have them.
unless(&ck_osparms){
	&osparms;
	&ck_osparms or die "$0: failed to pass &ck_osparms, aborting";
};

# let's warn about these condition only once, so we're not too annoying
for my $acfile ($ftpusers,$cron_allow,$at_allow){
	if(! -f $acfile){
		warn "$0: $acfile doesn't exist.\n";
	};
};

if(!defined($restore_directory)){
	#we're not restoring login(s)

	#step through any login name(s)
	for my $login (@ARGV){

		# sanity check the login name
		unless(&login_name_ok($login)){
			warn "$0: skipping login $login.\n";
			$error=1;
			next;
		};

		# have we already processed $login?
		if(exists($logins_done{$login})){
			warn "$0: duplicate, already processed login $login, skipping.\n";
			$error=1;
			next;
		};

		# track in hash that we've "done" this $login
		$logins_done{$login}=undef;
		# we track this now, rather than later, in case for some reason
		# we don't complete this login, if it's given additional times
		# as an argument, we'll recognize that we already did or attempted it

		# try to lookup $login
		if(!(@pwent=getpwnam($login))){
			warn "$0: unable to find login $login, skipping.\n";
			$error=1;
			next;
		};

		# At this point, we don't know if our getpwnam data came from
		# files or other authentication source(s).  For now we'll
		# presume it's file based, and later, when we attempt to
		# change relevant data, we'll check that getpwnam() picks up the
		# changes.

		my $uid=$pwent[2];

		# sanity check the uid
		unless(&uid_ok($uid)){
			warn "$0: for login $login, uid $uid not in acceptable range, skipping.\n";
			$error=1;
			next;
		};

		if(defined($save_directory)){
			length($save_directory) &&
			(
				-d $save_directory ||
				&my_mkdir('-p','--exists_ok',$save_directory)
			)
			or
			die "$0: failed to make --save directory $save_directory, aborting\n";
		};

		my $save_directory_login;

		if(defined($save_directory)){
			$save_directory_login=$save_directory . '/' . $login;
			unless(mkdir($save_directory_login,0777)){
				warn "$0: mkdir($save_directory_login) failed, skipping login $login.\n";
				$error=1;
				next;
			};
		};

		$verbose>=2 && print "$0: processing login $login\n";
		if(!$nochange && defined($save_directory)){
			$verbose>=3 && print "$0: saving password hash for $login\n";
			# save password hash
			unless(&savepwhash("$pwent[1]","$save_directory_login/$savepwhash")){
				warn "$0: failed to save password hash, skipping login $login.\n";
				$error=1;
				next;
			};
		};

		if(!$nochange){
			# lock password
			$verbose>=3 && print "$0: locking password for $login\n";
			unless(&setpwhash($login,$unmatchable_pwhash)){
				warn "$0: failed to set unmatchable password, skipping login $login.\n";
				$error=1;
				next;
			};
		};

		if(-x '/usr/lbin/getprpw' && -x '/usr/lbin/modprpw'){
			# administrative locks may apply
			# some of the actions and return values of these subroutines
			# and the particular logic used may not be particularly
			# inuitive until you keep in mind they're optimized to
			# minimize external calls to /usr/lbin/{getprpw,modprpw}
			if(!$nochange){
				if(defined($save_directory)){
					$verbose>=3 && print "$0: saving administrative lock status for $login\n";
					my $alock_status=&savealock($login,"$save_directory_login/$savealock");
					if(!defined($alock_status)){
						warn "$0: failed to determine and save administrative lock status, skipping login $login.\n";
						$error=1;
						next;
					}elsif($alock_status){
						$verbose>=3 && print "$0: setting administrative lock for $login\n";
						unless(&setalock($login)){
							warn "$0: failed to set administrative lock, skipping login $login.\n";
							$error=1;
							next;
						};
					#else already locked (nothing to do)
					};
				}else{
					my $is_not_alocked=&is_not_alocked($login);
					if(!defined($is_not_alocked)){
						warn "$0: failed to determine administrative lock status, skipping login $login.\n";
						$error=1;
						next;
					}elsif($is_not_alocked){
						$verbose>=3 && print "$0: setting administrative lock for $login\n";
						unless(&setalock($login)){
							warn "$0: failed to set administrative lock, skipping login $login.\n";
							$error=1;
							next;
						};
					#else already locked (nothing to do)
					};
				};
			};
		};

		if(!$nochange){
			if(defined($save_directory)){
				$verbose>=3 && print "$0: saving HOME for $login\n";
				# save HOME
				unless(&savehome("$pwent[7]","$save_directory_login/$savehome")){
					warn "$0: failed to save HOME, skipping login $login.\n";
					$error=1;
					next;
				};
			};
			$verbose>=3 && print "$0: locking HOME for $login\n";
			unless(&lockhome($login)){
				warn "$0: failed to lock HOME, skipping login $login.\n";
				$error=1;
				next;
			};
		};

		if(!$nochange){
			$verbose>=3 && print "$0: checking ftpusers status for $login\n";
			if(-f $ftpusers && &fgrep($login,$ftpusers)==0){
				# $login is not in $ftpusers, therefore allowed
				if(defined($save_directory)){
					$verbose>=3 && print "$0: saving ftpusers status for $login\n";
					unless(&mkfile("$save_directory_login/$saveftpusers")){
						warn "$0: failed to save ftpusers status, skipping login $login.\n";
						$error=1;
						next;
					};
				};
			};

			my $ret=&add_line_to_file_if_not_there($login,$ftpusers);
			if(!defined($ret)){
				warn "$0: failed to place/confirm login $login in $ftpusers, skipping login $login.\n";
				$error=1;
				next;
			}elsif($ret){
				$verbose>=3 && print "$0: added $login to ftpusers\n";
			#else was already in $ftpusers, nothing to do
			};
		};

		if(!$nochange){
			if(defined($save_directory)){

				# save cron/at allow/deny
				unless(&save_cron_at_allow($login,$save_directory_login)){
						
					warn "$0: failed to save cron/at allow/deny status, skipping login $login.\n";
					$error=1;
					next;
				};

				# save crontab, if applicable and non-null
				$verbose>=3 && print "$0: saving crontab for $login\n";
				my $crontab=&getcrontab($login);
				if(
					$crontab &&
					length($crontab) &&
					! 	&savecrontab(
							$crontab,
							"$save_directory_login/$savecrontab"
						)
				){
					warn "$0: failed to save crontab, skipping login $login.\n";
					$error=1;
					next;
				};

				# save shell
				$verbose>=3 && print "$0: saving shell for login $login\n";
				unless(
					&saveshell(
						"$pwent[8]",
						"$save_directory_login/$saveshell"
					)
				){
					warn "$0: failed to save shell, skipping login $login.\n";
					$error=1;
					next;
				};

				#disable at/batch access
				$verbose>=3 && print "$0: disabling at/batch for login $login\n";
				unless(&set_at_deny($login)){
					warn "$0: failed to disable at/batch for login $login, skipping login $login.\n";
					$error=1;
					next;
				};
			};
		};
		my @signals=@signals_nice; # we'll be somewhat nice the first time

		my $race_is_on=1; # race conditions do or may still apply
		# there are almost certainly race conditions here, so we
		# loop until we're reasonably asured everything where a race
		# condition may exist has been locked out
		{
			my $jobs=undef;
			do{
				#determine and remove at/batch jobs
				$jobs=&$get_at_jobs($login);
				###UNFINISHED###
				if(!defined($jobs)){
					$error=1;
					warn "$0: failed to determine at/batch jobs for login $login, skipping login $login.\n";
					next;
				};
				for(@$jobs){
					unless(&$remove_at_job($_)){
						# we'll gripe about this a bit, but not treat it as
						# an error, as the at/batch job may legitimately no
						# longer exist by the time we attempt to remove it
						warn "$0: NOTICE: failed to remove at/batch job $_ for login $login (perhaps it was already gone?)\n";
					};
				};

				my $crontab_file="$crontabs_dir/$login";

				# disable cron access and remove cron jobs
				# also create "lock" as part of beating "race" condition(s)
				if($remove_crontab_needs_allow){

					# repeat this until we've successfully created our
					# crontab "lock" file - we'll presume we've "won" the
					# crontab "race" when we've successfully created the
					# "lock" file *and* we've killed all of $login's PIDs
					do{
						if(-f $crontab_file){
							unless(&set_cron_allow($login)){
								$error=1;
								warn "$0: set_cron_allow failed for login $login, skipping login $login\n";
								next;
							};
							my $ret=system(&$remove_crontab_args($login));
							if($ret){
								warn ("$0: system (",join(',',&$remove_crontab_args($login)),") failed",($ret==-1?(": $!"):(", returned: " . ($ret>>8))),"\n");
								$error=1;
							};

						};
						if(
							mkdir($crontab_file,0777)
							&&
							! &set_cron_deny($login)
						){
								$error=1;
								warn "$0: set_cron_deny failed for login $login, skipping login $login\n";
								next;
						};
					} until(-d $crontab_file);

				}else{

					unless(&set_cron_deny($login)){
						$error=1;
						warn "$0: set_cron_deny failed for login $login, skipping login $login\n";
						next;
					};

					# repeat this until we've successfully created our
					# crontab "lock" file - we'll presume we've "won" the
					# crontab "race" when we've successfully created the
					# "lock" file *and* we've killed all of $login's PIDs
					do{
						if(-f $crontab_file){
							my $ret=system(&$remove_crontab_args($login));
							if($ret){
								warn ("$0: system (",join(',',&$remove_crontab_args($login)),") failed",($ret==-1?(": $!"):(", returned: " . ($ret>>8))),"\n");
								$error=1;
							};
						};
						mkdir($crontab_file,0777);
					} until(-d $crontab_file);

				};

				# kill processes
				unless(&kill_uid_signals($uid,@signals)){
					$error=1;
					warn "$0: failed to signal PIDs of login $login UID $uid, skipping login $login\n";
					next;
				};

				# had their chance, no more Mr. Nice Guy
				@signals=@signals_nasty;

				# remove at/batch jobs (if there are any to remove, race continues)
				$jobs=&$get_at_jobs($login);
				###UNFINISHED###
				if(!defined($jobs)){
					$error=1;
					warn "$0: failed to determine at/batch jobs for login $login, skipping login $login.\n";
					next;
				}elsif(!@$jobs){
					# no at/batch jobs left for $login
					if(-d $crontab_file){
						# having met both those conditions and after killing
						# PIDs, we've won the race (as long as we can
						# remove our temporary "lock" "file" (directory) okay)
						if(rmdir($crontab_file)){
							# beat the race okay
							$race_is_on=0;
						}else{
							$error=1;
							warn "$0: rmdir($crontab_file) failed: $!, skipping login $login.\n";
							next;
						};
					};
				}else{
					for (@$jobs){
						unless(&$remove_at_job($_)){
							# we'll gripe about this a bit, but not treat it as
							# an error, as the at/batch job may legitimately no
							# longer exist by the time we attempt to remove it
							warn "$0: NOTICE: failed to remove at/batch job $_ for login $login (perhaps it was already gone?)\n";
						};
					};
				};
			}while($race_is_on);
		};

		if(!$nochange){
			# lock shell
			# we handle this after possible (PID) race condition stuff, as
			# some flavors/configurations may allow user to change their
			# login shell
			$verbose>=3 && print "$0: locking shell for $login\n";
			unless(&setshell($login,$lockedshell)){
				warn "$0: failed to lock shell, skipping login $login.\n";
				$error=1;
				next;
			};
		};
	};

	#step through any uids not already covered
	for my $uid (@uid){
		if($nochange){
			next;
		};
		$verbose>=2 && print "$0: processing uid $uid\n";
		# kill processes
		unless(&kill_uid_signals($uid,@signals_nice)){
			$error=1;
			warn "$0: failed to signal PIDs of UID $uid, skipping UID $uid\n";
			next;
		};
	};
	# there really isn't anything else we want to do based upon a UID alone

}else{
	$verbose && print "$0: --restore ...\n";
	#defined($restore_directory)
	unless(opendir(RESTORE_DIRECTORY,$restore_directory)){
		die "$0: opendir($restore_directory) failed, aborting\n";
	};
	while(defined(my $login=readdir(RESTORE_DIRECTORY))){
		$verbose>=6 && print "$0: found $login in --restore directory\n";
		if($login eq '.' || $login eq '..'){
			#quetly skip current and parent directory entries
			$verbose>=7 && print "$0: skipping $login in --restore directory\n";
			next;
		};
		unless(&login_name_ok($login)){
			warn "$0: skipping login $login.\n";
			$error=1;
			next;
		};
		my $restore_directory_login=$restore_directory . '/' . $login;
		unless(-d $restore_directory_login){
			warn "$0: $restore_directory_login doesn't appear to be a directory, skipping login $login.\n";
			$error=1;
			next;
		};

		$verbose>=2 && print "$0: about to process $login in --restore directory\n";

		$verbose>=3 && print "$0: restoring shell for $login\n";
		unless(&restoreshell($login,"$restore_directory_login/$saveshell")){
			warn "$0: failed to restore shell for login $login.\n";
			$error=1;
		};

		$verbose>=3 && print "$0: restoring HOME for $login\n";
		unless(&restorehome($login,"$restore_directory_login/$savehome")){
			warn "$0: failed to restore HOME for login $login.\n";
			$error=1;
		};

		$verbose>=3 && print "$0: restoring password hash for $login\n";
		unless(&restorepwhash($login,"$restore_directory_login/$savepwhash")){
			warn "$0: failed to restore password hash for login $login.\n";
			$error=1;
		};

		#restore administrative lock status
		if(
			-f "$restore_directory_login/$savealock"
		){
			# clear the administrative lock
			unless(&setalock($login,'alock=NO')){
				warn "$0: falied to clear administrative lock for $login\n";
				$error=1;
			};
		}#else
			# no change needed
		;

		#restore ftpusers status
		if(
			-f "$restore_directory_login/$saveftpusers" &&
			&fgrep($login,$ftpusers)
		){
			if(&remove_line_from_file_if_there($login,$ftpusers)){
				$verbose>=3 && print "$0: restored ftpusers status for $login\n";
			}else{
				warn "$0: failed to restore ftpusers status for $login\n";
				$error=1;
			};
		}#else
			# no change needed
		;

		#restore saved crontab
		if(-f "$restore_directory_login/$savecrontab"){
			unless(&putcrontab($login,"$restore_directory_login/$savecrontab")){
				warn "$0: putcrontab failed for login $login, skipping login $login\n";
				unless(&set_cron_deny($login)){
					warn "$0: set_cron_deny failed for login $login, skipping login $login\n";
				};
				$error=1;
				next;
			};
		};

		#restore cron/at allow/deny status
		$verbose>=3 && print "$0: restoring cron/at allow/deny for $login\n";
		if(-f "$restore_directory_login/$savecronallow"){
			unless(&set_cron_allow($login)){
				$error=1;
				warn "$0: set_cron_allow failed for login $login, skipping login $login\n";
				next;
			};
		}else{
			unless(&set_cron_deny($login)){
				$error=1;
				warn "$0: set_cron_deny failed for login $login, skipping login $login\n";
				next;
			};
		};
		if(-f "$restore_directory_login/$saveatallow"){
			unless(&set_at_allow($login)){
				$error=1;
				warn "$0: set_at_allow failed for login $login, skipping login $login\n";
				next;
			};
		}else{
			unless(&set_at_deny($login)){
				$error=1;
				warn "$0: set_at_deny failed for login $login, skipping login $login\n";
				next;
			};
		};

	};
	unless(closedir(RESTORE_DIRECTORY)){
		die "$0: closedir($restore_directory) failed, aborting\n";
	};
};

exit $error;

########################################
# various stuff to save / lock / lockout
########################################
# lock out
# 	first via stuff user can't change
# 		make password unmatchable/locked*
# 		set administrative lock(s)*
# 		home directory (change HOME in /etc/passwd, so we lock access
# 			via this host, but don't change access or location of that
# 			directory relative to other hosts)*
# 		ftpusers*
#		*save cron/at allow/deny
#		*save crontab
#		*save shell
# 		disable at/batch access
# 	then stuff where race conditions may exist:
#		determine and remove at/batch jobs
# 		disable cron access and remove cron jobs
# 			create temporarly lock
# 		kill processes
#		remove at/batch jobs (if there were any to remove, race continues)
#		remove temporary lock if applicable
# 	disable shell
# *(optionally) backup stuff
# ($name,$passwd,$uid,$gid,$quota,$comment,$gcos,$dir,$shell,$expire) = getpw*

########################################
# --restore
########################################
#restore shell
#restore home directory
#restore password hash
#restore administrative lock status
#restore ftpusers status
#restore saved crontab
#restore cron/at allow/deny status
