#! /usr/local/bin/perl

use Time::HiRes 'gettimeofday';
use Getopt::Long;
Getopt::Long::config qw(bundling no_getopt_compat);

sub opt($$) { push @opt, "-$_[0]$_[1]" }

GetOptions \%opt,
    qw'a b=s C d D=i e f j k+ l L N P S T v W',
    'I' => sub { push @opt, "-u$ENV{LOGNAME}" },
    'K=s' => \@kill,
    'o=s' => sub { push @opt, ($_[1] =~ /\bpid\b/) ? "-o$_[1]" : "-opid,$_[1]" },
    'p=s' => \&opt,
    'r' => sub { push @opt, '-uroot' },
    't=s' => \&opt,
    'u=s' => \&opt,

    'help|?' => sub { print STDERR <<'EOF'; exit };
usage: p[ -aefIjlLrkdv -o<opt> -u<user> -p<pid> -t<tty> -K<sig> -D<seconds> -b<bsdopt>][ pattern ...]
    -I	show my processes
    -r	show root processes
    -k	kill, repeat suppresses question
    -K	kill with signal, can be repeated and -k suppresses question
    -C	sorted by CPU
    -N	sorted by nice
    -P	sorted by prio
    -S	sorted by size
    -T	sorted by start time
    -d	diff (default: every second)
    -D	diff or loop time interval
    -v	vice versa, show processes not matching patterns
    -b	call bsd/ucb ps with bsd options
EOF

push @opt, "-$opt"
    if $opt = join '', grep $opt{$_}, qw'a e f j l L';
$optD = defined( $opt{D} ) ? $opt{D} : 1;

my $re = 0;
if( @ARGV ) {
    local $" = '|';
    $re = qr/@ARGV/;
}

my $linux = 'linux' eq $^O;

my( $pidcol, $delcol, $sortcol, $ps );
my $cmd = 'ps';
if( $opt{b} ) {
    $cmd = '/usr/ucb/ps' unless $linux;
    @opt = $linux ? $opt{b} : "-$opt{b}";
}

AGAIN:
# 5.8.0: $ps = open PS, '-|', $cmd, @opt;
$ps = open PS, '-|' or
    exec $cmd, @opt;

AGAIN0:
my $found = 2;
if( @time ) {
    <PS>;
} else {
    $head = <PS>;
    $delcol = index $head, ' C ';
    $pidcol = index $head, '  PID';
    $head =~ s/ C //;
    $sortcol = index $head,
	$opt{C} ? ($linux ? '    TIME' : ' TIME') :
	$opt{T} ? ($linux ? 'STIME' : '   STIME') :
	$opt{W} ? ($linux ? 'WCHAN' : '   WCHAN') :
	$opt{S} ? ($linux ? 'DR SZ ' : '    SZ') :
	$opt{P} ? ' PRI ' :
	$opt{N} ? ' NI ' : '';
}
@ps = grep {
    $ok = 1;
    s! inet(/\d+)!" in$1" . (' ' x (5 - length $1))!e;
    if( $found ) {
	$pid = int substr $_, $pidcol;
	$found--, $ok = 0 if $pid == $$ || $pid == $ps;
    }
    $ok = $opt{v} ? ($_ !~ $re) : ($_ =~ $re)
	if $re && $ok;
    substr( $_, $delcol, 3 ) = '' if $ok && $delcol > 0;
    $ok;
} <PS>;

if( $opt{d} || defined $opt{D} ) {
    close PS;
    if( !$optD ) {		# on 0 delay fire next subprocess asap
	$ps = open PS, '-|' or
	    exec $cmd, @opt;
    }
    if( $opt{d} ) {
	%ps = ();
	$ps{int substr $_, $pidcol} = $_
	    for @ps;
	if( %oldps ) {		# previous round output something
	    $sep = sprintf '*** ' . substr( localtime $time[0], 4, 15 ) . ".%06d ***\n", $time[1]
		if @time;
	    $last = -1;
	    for( sort { $a <=> $b } keys %ps, keys %oldps ) {
		next unless $last < $_;
		$last = $_;
		if( $ps{$_} ) {
		    next if $ps{$_} eq $oldps{$_};
		    print $sep, $oldps{$_} ? '  ' : '+ ', $ps{$_};
		} else {
		    print "$sep- $oldps{$_}";
		}
		$sep = '';
	    }
	} elsif( @ps ) {
	    print '  ', $head if !@time;
	    print '  ', $ps{$_} for sort { $a <=> $b } keys %ps;
	}
	%oldps = %ps;
    } elsif( @ps ) {		# found some
	%ps = ();
	$ps{int substr $_, $pidcol} = $_
	    for @ps;
	if( @time ) {
	    printf '*** ' . substr( localtime $time[0], 4, 15 ) . ".%06d ***\n", $time[1];
	} else {
	    print $head;
	}
	print $ps{$_} for sort { $a <=> $b } keys %ps;
    }
    if( $optD ) {
	sleep $optD;
	@time = gettimeofday;
	goto AGAIN;
    } else {
	@time = gettimeofday;
	goto AGAIN0;
    }
}

# normal single shot operation
print $head,
    $sortcol > 0 ? sort { substr( $a, $sortcol ) cmp substr $b, $sortcol } @ps : sort @ps
    if @ps;


if( @ps and $opt{k} || @kill ) {
    my @pids = map { int substr $_, $pidcol } @ps;
    my $kill = ($opt{k} > 1 or $opt{k} && @kill);
    unless( $kill ) {
	local $| = 1;
	$kill = join ' -', '', @kill;
	print "kill$kill @pids? ";
	$kill = (<STDIN> =~ /^[jy]/);
    }
    if( $kill ) {
	@kill = 15 unless @kill;
	kill $_ => @pids for @kill;
	goto AGAIN;
    }
}

__END__

=head1 ps wrapper

=over 4

=item *

sorts by pid or other column you specify (finding them from the headline, no
matter what your variant of ps does)

=item *

eliminates itself and ps process from output

=item *

options for all own (-I) or root's (-r) processes

=item *

allows grepping processes (optionally inversely) with Perl regexps

=item *

eliminates C column, which is by definition useless

=item *

loop mode repeatedly outputs every n seconds, specially optimized for 0
seconds to loop as fast as possible -- interesting when grepping for running
and/or runnable processes

=item *

loop mode with diff to previous output allows tracking processes as they appear
(+) and dissapear (-) or change in some dispayed parameter

=back

=begin CPAN

=head1 README

B<ps wrapper>
B< · >sort by pid or other column
B< · >skip self and ps
B< · >grep (-v)
B< · >loop mode w/ or w/o diff to previous

=pod SCRIPT CATEGORIES

UNIX/System_administration
