#!/usr/bin/env perl #ABSTRACT: List your jobs (or others), and delete them if you wish #PODNAME: lsjobs use v5.12; use warnings; use Getopt::Long; use FindBin qw($RealBin); use Data::Dumper; use Term::ANSIColor qw(:constants); use File::Basename; use Text::ASCIITable; $Data::Dumper::Sortkeys = 1; if (-e "$RealBin/../dist.ini") { say STDERR "[dev mode] Using local lib" if ($ENV{"DEBUG"}); use lib "$RealBin/../lib"; } use NBI::Slurm; use Cwd; my $current_slurm_jobid = $ENV{SLURM_JOBID} // -1; my $unix_username = $ENV{USER}; my $user_home_dir = $ENV{HOME}; my $opt_user = $unix_username; my $opt_status = '.+'; my $opt_running_bool = 0; my $opt_pending_bool = 0; my $opt_delete_bool = 0; my $opt_verbose_bool = 0; my $opt_queue = '.+'; my $opt_name = '.+'; my $opt_tab = 0; GetOptions( 'u|user=s' => \$opt_user, 'n|name=s' => \$opt_name, 's|status=s'=> \$opt_status, 'r|running' => \$opt_running_bool, 'd|delete' => \$opt_delete_bool, 't|tab' => \$opt_tab, 'verbose' => \$opt_verbose_bool, 'version' => sub { say "lsjobs v", $NBI::Slurm::VERSION; exit }, 'help' => sub { usage() }, ); if (not NBI::Slurm::has_squeue()) { say STDERR RED, "Error:", RESET, " squeue not found in PATH. Are you in the cluster?"; exit 1; } my $jobs = getjobs(); my @ids = (); for my $positional (@ARGV) { if ($positional =~ /^(\d+)$/) { push(@ids, $1); } else { if ($opt_name eq '.+') { $opt_name = $positional; } else { say STDERR "Error: unknown positional argument: $positional"; usage(); } } } if ($opt_user eq 'ALL' or $opt_user eq 'all') { $opt_user = '.+'; } if ($opt_verbose_bool) { say STDERR "User: $opt_user"; say STDERR "Jobs: ", scalar(keys %{$jobs}); } my $selected_jobs = {}; my $selected_arrays = [['JobID', 'User', 'Queue', 'Name', 'State', 'Time', 'TotalTime', 'NodeList', 'CPUS', 'Memory', 'Reason'], ['-----', '----', '-----', '----', '-----', '----', '---------', '--------', '----', '------', '------']]; if ($opt_tab) { # Remove separator line, add "#" $selected_arrays->[0]->[0] = "#" . $selected_arrays->[0]->[0]; $selected_arrays = [ $selected_arrays->[0] ]; } for my $job (sort keys %{$jobs}) { # Check user (full match) if ($jobs->{$job}->{USER} !~ /^$opt_user$/) { next; } # Check queue (partial match ok) if ($jobs->{$job}->{PARTITION} !~ /$opt_queue/) { next; } # Check name if ($jobs->{$job}->{NAME} !~ /$opt_name/) { next; } # Check status if ($opt_pending_bool and $jobs->{$job}->{STATE} ne 'PENDING') { next; } if ($opt_running_bool and $jobs->{$job}->{STATE} ne 'RUNNING') { next; } if (scalar @ids > 0 and not grep {$_ eq $job} @ids) { next; } my $array = [$jobs->{$job}->{JOBID} =~/_/ ? substr($jobs->{$job}->{JOBID}, 0, index($jobs->{$job}->{JOBID}, '_'))."#" : $jobs->{$job}->{JOBID}, $jobs->{$job}->{USER}, $jobs->{$job}->{PARTITION}, $jobs->{$job}->{NAME}, $opt_tab ? $jobs->{$job}->{STATE} : state_string($jobs->{$job}->{STATE}), $jobs->{$job}->{TIME}, $jobs->{$job}->{TIME_LIMIT}, $jobs->{$job}->{NODELIST}, $jobs->{$job}->{"CPUS"}, $jobs->{$job}->{"MIN_MEMORY"}, $opt_tab ? $jobs->{$job}->{"REASON"} : reason_string($jobs->{$job}->{"REASON"}) ]; push(@{$selected_arrays}, $array); } if ($opt_tab) { for my $array (@{$selected_arrays}) { say join("\t", @{$array}); } } else { # Render default table render_table(@{$selected_arrays}); print RESET, "\n"; } ## Print single job if ($opt_verbose_bool and scalar @{$selected_arrays} == 3) { my $job = extractJobByID($jobs, $selected_arrays->[2]->[0]); for my $key (sort keys %{$job}) { # Filter useless values if ($job->{$key} =~ /^$/ or $job->{$key} =~ /^(\(null\)|\*)$/) { next; } if ($key =~/(S_C_T|USER|ACCOUNT)/) { next; } say YELLOW, sprintf("%-20s", $key), RESET, $job->{$key}; } } my @selected_ids = joblist_to_ids(@{$selected_arrays}); if ($opt_delete_bool and (scalar @selected_ids)) { say RED "\nDELETE JOBS:", RESET; if (prompt("Delete " . scalar(@selected_ids) . " jobs?", 'n') =~ /^(y|Y|yes|YES)$/) { my $command = "scancel " . join(" ", @selected_ids); system($command); if ($? == -1) { say RED, "ERROR", RESET ": Failed to delete: $!\n"; } } } elsif ($opt_delete_bool) { say STDERR "No jobs selected for deletion"; } sub state_string { my $s = shift; $s = substr($s, 0, 1); my $c = RESET; if ($s =~/^R/) { $c = GREEN . ON_BLACK; } elsif ($s =~/^P/) { $c = YELLOW . ON_BLACK; } else { $c = RED . ON_BLACK; } return $c . $s . WHITE . ON_BLACK; } sub reason_string { my $s = shift; my $c = RESET; if ($s =~/^None/) { $c = BLUE . ON_BLACK; } elsif ($s =~/^Priority/) { $c = YELLOW . ON_BLACK; } elsif ($s =~/^Bad/) { $c = WHITE . ON_RED; } else { $c = RED . ON_BLACK; } return $c . $s . RESET . ON_BLACK; } sub joblist_to_ids { # Receive a list of lists (all same length) and returns a list of jobids my @rows = @_; my @ids = (); # remove first two rows for my $row (@rows) { # Skip non numeric values next if ($row->[0] !~ /^\d+$/); push @ids, $row->[0]; } return @ids; } sub short_job { # Print a line of minimal information about a job my $line_width = get_terminal_width(); my $job = shift; my $jobid = $job->{JOBID}; my $name = $job->{NAME}; my $state = $job->{STATE}; my $user = $job->{USER}; my $queue = $job->{PARTITION}; my $time = $job->{TIME}; # Return a string sorther than $line_width my $line = sprintf("%-10s %-10s %-10s %-10s %-10s %-10s", $jobid, $name, $state, $user, $queue, $time); return $line; } sub render_table { # Receive a list of lists (all same length) and print a table not larger than $line_width # @_ is an array of array references my @rows = @_; my $n_cols = scalar(@{$rows[0]}); my $line_width = get_terminal_width() - $n_cols - 1; # For each column, evaluate the maximum string contained in that column my @max_widths = (); for my $col (0..$n_cols-1) { my $max_width = 0; for my $row (@rows) { my $width = ascii_len($row->[$col]); $max_width = $width if ($width > $max_width); } push(@max_widths, $max_width); } # Now print the table for my $row (@rows) { my $line = WHITE . ON_BLACK; for my $col (0..$n_cols-1) { my $width = $max_widths[$col]; my $cell = $row->[$col]; my $stripped = $cell; $stripped =~ s/\e\[[0-9;]*m//g; my $tmpline .= sprintf("|%-${width}s ", $stripped); # In tmpline replace $stripped with $cell, without using regex my $index = index($tmpline, $stripped); substr($tmpline, $index, length($stripped), $cell); $line .= $tmpline; } say $line, "|"; } print RESET; } sub ascii_len { my $string = shift; # Return legnth excluding ANSI escape sequences $string =~ s/\e\[[0-9;]*m//g; return length($string); } sub extractJobByID { my ($jobs, $id) = @_; my $job = {}; for my $jobid (keys %{$jobs}) { if ($jobid eq $id) { $job = $jobs->{$jobid}; last; } } return $job; } sub getjobs { # Create an anonymous hash, and return it my $jobs = {}; my $cmd = q(squeue --format='%all'); my @output = `$cmd`; my $c = 0; my @header = (); for my $line (@output) { chomp $line; my @fields = split(/\|/, $line); $c++; if ($c == 1 ) { # Field names for my $field (@fields) { push(@header, stripchars($field)); } } else { # Job info my $job = {}; if (scalar(@fields) != scalar(@header)) { say STDERR "Error: number of fields in header and line do not match"; say STDERR "Header: ", scalar(@header); say STDERR "Line: ", scalar(@fields); say STDERR "Line: $line"; exit; } for my $i (0..$#header) { $job->{"$header[$i]"} = $fields[$i] if (not defined $job->{"$header[$i]"}); } $jobs->{$job->{JOBID}} = $job; } } return $jobs; } sub get_terminal_width { my $terminal_width = `tput cols`; chomp($terminal_width); return $terminal_width > 20 ? $terminal_width : 80; } sub stripchars { my $string = shift; # replace non alphanumeric characters with _ $string =~ s/[^A-Za-z0-9]/_/g; return $string; } sub prompt { my ($message, $default) = @_; my $prompt = "$message [$default]: "; print $prompt; my $answer = <STDIN>; chomp $answer; $answer = $default if ($answer eq ''); return $answer; } sub usage { say <<END; Usage: lsjobs [options] [jobid ... | pattern ] ---------------------------------------------- Options: -u, --user <username> Show only jobs from this user [default: $unix_username] Type 'all' to show all users -n, --name <pattern> Show only jobs with this name [default: .+] -s, --status <pattern> Show only jobs with this status [default: .+] -r, --running Show only running jobs -p, --pending Show only pending jobs -t, --tab Output in simple TSV format (pipe to vd for interactive table) -d, --delete Delete the selected jobs --verbose Show verbose output END exit; } __END__ =pod =encoding UTF-8 =head1 NAME lsjobs - List your jobs (or others), and delete them if you wish =head1 VERSION version 0.11.0 =head1 SYNOPSIS lsjobs [options] [jobid ... | pattern] =head1 DESCRIPTION This script lists the jobs and provides the option to delete them. It allows filtering the jobs based on various criteria such as user, name, and status. =head1 OPTIONS =over 4 =item B<-u, --user <username>> Show only jobs from the specified user. Default: current user. =item B<-n, --name <pattern>> Show only jobs with the specified name pattern. Default: .+ (matches any name). =item B<-s, --status <pattern>> Show only jobs with the specified status pattern. Default: .+ (matches any status). =item B<-r, --running> Show only running jobs. =item B<-t, --tab> Output in simple TSV format (tip: pipe to C<vd> for interactive table) =item B<-d, --delete> Delete the selected jobs. This option must be used with caution, but an interactive prompt is provided =item B<--verbose> Display verbose output. If a single job is selected, for example by giving a precise ID, the full job details will be displayed. =item B<--help> Print the help message and exit. =back =head1 ARGUMENTS =over 4 =item B<jobid ... | pattern> Optional. Specify either job IDs (many) or a pattern (single) to filter the jobs based on their names. =back =head1 EXAMPLES =over 4 =item B<Example 1:> List all jobs: lsjobs =item B<Example 2:> List jobs with the name "myjob": lsjobs -n myjob =item B<Example 3:> List running jobs of a specific user: lsjobs -r -u username =item B<Example 4:> Delete some of my jobs (only pending, and with name containing MEGAHIT): lsjobs -d --pending MEGAHIT =back =head1 AUTHOR Andrea Telatin <proch@cpan.org> =head1 COPYRIGHT AND LICENSE This software is Copyright (c) 2023-2025 by Andrea Telatin. This is free software, licensed under: The MIT (X11) License =cut