11

I have:

sleep 210m && for i in $(seq 1 5); do echo -e '\a'; sleep 0.5; done 

running as a simple, no-frills timer to remind me when something should be done. That sleep 210m is PID 25347.

I'm trying to figure out how much time is left in the sleep. The best I've come up with, with me putting in the original sleep amount (210 minutes) is:

$ echo "210 60 * $(ps -o etimes= 25347) - 60 ~ r n [:] P p" | dc
78:11

(Explanation: First bit computes the number of seconds in the original sleep; the $(ps…) bit gets the time since sleep started in seconds, then the rest subtracts them and displays in minutes and seconds.)

It occurs to me this would be useful in general; is there a better way to do this? Or at least a clever way to parse the sleep time from ps -o args?

derobert
  • 107,579
  • 20
  • 231
  • 279
  • 1
    Note that processes can (and often do) run more than one command in their lifetime. For instance in `sh -c 'sleep 1; sleep 2'` with many `sh` implementations, it's the same process that executes `sh` and later executes `sleep 2` (1 second later). – Stéphane Chazelas Oct 05 '16 at 16:27
  • @StéphaneChazelas I don't think that can happen here, presumably the shell can only do that optimization on the last command it's executing (as it has no way to regain control after `exec`). But that's a good point for any general solution. – derobert Oct 05 '16 at 16:33
  • Also note that several shells (`mksh` and `ksh93` at least) have `sleep` built-in (so wouldn't show-up in `ps`)., you'd need to know in advance which `sleep` implementations you're dealing with. – Stéphane Chazelas Oct 05 '16 at 16:37

5 Answers5

6

Supporting GNU or Solaris 11 sleep arguments (one or more <double>[smhd] durations, so would also work with traditional implementations that support only one decimal integer number (like on FreeBSD), but not with those accepting more complex arguments like ISO-8601 durations). Using etime instead of etimes as that's more portable (standard Unix).

remaining_sleep_time() { # arg: pid
  ps -o etime= -o args= -p "$1" | perl -MPOSIX -lane '
    %map = qw(d 86400 h 3600 m 60 s 1);
    $F[0] =~ /(\d+-)?(\d+:)?(\d+):(\d+)/;
    $t = -($4+60*($3+60*($2+24*$1)));
    for (@F[2..$#F]) {
      s/\?//g;
      ($n, $p) = strtod($_);
      $n *= $map{substr($_, -$p)} if $p;
      $t += $n
    }
    print $t'
}

(the s/\?//g is to get rid of the ? characters that procps' ps uses as replacement for control characters. Without it, it would fail to parse sleep $'\r1d' or sleep $'\t1d'... Unfortunately, in some locales, including the C locale, it uses . instead of ?. Not much we can do in that case as there's no way to tell a \t5d from a .5d (half day)).

Pass the pid as argument.

That also assumes the argv[0] passed to sleep doesn't contain blanks and that the number of arguments is small enough that it's not truncated by ps.

Examples:

$ sleep infinity & remaining_sleep_time "$!"
Inf
$ sleep 0xffp-6d &
$ remaining_sleep_time "$!"
344249
$ sleep 1m 1m 1m 1m 1m & remaining_sleep_time "$!"
300

For a [[[ddd-]HH:]MM:]SS output instead of just the number of seconds, replace the print $t with:

$output = "";
for ([60,"%02d\n"],[60,"%02d:"],[24,"%02d:"],[inf,"%d-"]) {
  last unless $t;
  $output = sprintf($_->[1], $t % $_->[0]) . $output;
  $t = int($t / $_->[0])
}
printf "%s", $output;
Stéphane Chazelas
  • 522,931
  • 91
  • 1,010
  • 1,501
  • 1
    `0xffp-6d` ... Now, that's a test case! But actually GNU sleep takes things like `sleep 1h 30m` as well... – derobert Oct 06 '16 at 16:55
  • @derobert. D'oh I could have sworn I'd tried that. I guess I must have tried `sleep 1h -1m` which works on Solaris 11 but not with GNU `sleep`. Back to the drawing board. At least it doesn't support the iso8601 durations like ksh93 which would be a lot harder. – Stéphane Chazelas Oct 06 '16 at 20:02
  • @derobert, updated to support several arguments – Stéphane Chazelas Oct 06 '16 at 20:17
  • @StéphaneChazelas your solution is good, though it won't work in case `sleep` is put on pause. It might even display negative value in this case. – rush Apr 03 '18 at 19:36
  • Have been using this fine for a week but today `remaining_sleep_time 12838` returned `-40642`. It might be on "Pause" as @rush states but not sure how to debug? – WinEunuuchs2Unix Sep 19 '18 at 00:29
  • @WinEunuuchs2Unix What's the output of `ps -o etime,args -p 12838`? – Stéphane Chazelas Sep 19 '18 at 06:31
  • @StéphaneChazelas There was a bug where the bash daemon (started by `/etc/cron.d`) was exiting as no terminal was attached. I removed the bug last night and cannot reproduce the error ATM. – WinEunuuchs2Unix Sep 19 '18 at 10:29
3

This seemed like a fun problem to solve; since thrig covered a perl option, here's a bash script that does something similar. It does not do enough error-checking (it assumes that you're passing in a valid PID of a sleep command). It handles the same syntax that GNU's coreutils sleep does, namely:

  • s|m|h|d suffixes for seconds/minutes/hours/days
  • multiple time parameters get added together

#!/usr/bin/env bash

# input: PID of a sleep command
# output: seconds left in the sleep command

function parse_it_like_its_sleep {
  # $1 = one sleep parameter
  # print out the seconds it translates to

  mult=1
  [[ $1 =~ ([0-9][0-9]*)(s|m|h|d) ]] && {
    n=${BASH_REMATCH[1]}
    suffix=${BASH_REMATCH[2]}
  } || {
    n=$1
  }
  case $suffix in
    # no change for 's'
    (m) mult=60;;
    (h) mult=$((60 * 60));;
    (d) mult=$((60 * 60 * 24));;
  esac
  printf %d $((n * mult))
}

# TODO - some sanity-checking for $1
set -- $(ps -o etimes=,args= $1)
[[ $2 = "sleep" ]] || exit 1
elapsed=$1
shift 2
total=0
for arg
do
  # TODO - sanity-check $arg
  s=$(parse_it_like_its_sleep $arg)
  total=$((total + s))
done
printf "%d seconds left\n" $((total - elapsed))
Jeff Schaller
  • 66,199
  • 35
  • 114
  • 250
  • Even if limited to GNU sleep (some sleep implementations like Solaris 11 or ksh93 builtin support much more complex duration expressions), that's incomplete as it doesn't work for floating point numbers (like `sleep 0.5d` or `sleep 1e5` or `sleep infinity`). Switching from `bash` to a shell supporting floating points like zsh, ksh93 or yash would help. Not that `etimes` is not portable. – Stéphane Chazelas Oct 06 '16 at 08:24
  • The rabbit hole for parsing floating point went deeper than I was willing to go, so I may leave this here with the known limitations, and the fact that it's just a minor improvement over the OP's suggestion (this can determine the sleep time from the PID versus hard-coding that in). – Jeff Schaller Oct 06 '16 at 16:09
  • [Easier with `perl`](/a/314777) – Stéphane Chazelas Oct 06 '16 at 16:39
2

If you're looking for a simple solution, you can just use ps aux, which shows the START time. But you might want to use it only for a quick approximation rather ran a precise value.

In this example, the start time is 15:47. So the sleep will end around 15:57

enter image description here

Hakro
  • 121
  • 2
1

This might be better done with a script that can show the time remaining when bonked with a QUIT (usually control+\) or INFO signal.

#!/usr/bin/env perl
#
# snooze - sleep for a given duration, with SIGINFO or SIGQUIT
# (control+\ typically) showing how much time remains. Usage:
#
#   snooze 3m; make-noise-somehow
#
# or with
#
#   snooze 25m bread; make-noise-somehow
#
# one can then elsewhere
#
#   pkill -INFO snooze-bread

use strict;
use warnings;
use Term::ReadKey qw(ReadMode);

my %factors = ( s => 1, m => 60, h => 3600, d => 86400 );

my $arg = shift or die "Usage: $0 sleep-time [label]\n";
my $to_sleep = 0;
while ( $arg =~ m/([0-9]+)([smhd])?/g ) {
    my $value  = $1;
    my $factor = $2;
    $value *= $factors{$factor} if $factor;
    $to_sleep += $value;
}
die "nothing to die to sleep to sleep no more for\n" if $to_sleep == 0;

my $label = shift;
$0 = $label ? "snooze-$label" : "snooze";

ReadMode 2;    # noecho to hide control+\s from gunking up the message

sub remainder { warn "$0: " . deltatimefmt($to_sleep) . " remaining\n" }

sub restore {
    ReadMode 0;
    warn "$0: " . deltatimefmt($to_sleep) . " remainds\n";
    exit 1;
}

# expect user to mash on control+\ or whatever generates SIGINFO
for my $name (qw/ALRM INFO QUIT/) {
    $SIG{$name} = \&remainder;
}

# back to original term settings if get blown away
for my $name (qw/HUP INT TERM USR1 USR2/) {
    $SIG{$name} = \&restore;
}

$SIG{TSTP} = 'IGNORE';    # no Zees for you!

while ( $to_sleep > 0 ) {
    $to_sleep -= sleep $to_sleep;
}

ReadMode 0;
exit;

sub deltatimefmt {
    my $difference = shift;

    return "0s" if $difference == 0;

    my $seconds = $difference % 60;
    $difference = ( $difference - $seconds ) / 60;
    my $minutes = $difference % 60;
    $difference = ( $difference - $minutes ) / 60;

    #  my $hours = $difference;
    my $hours = $difference % 24;
    $difference = ( $difference - $hours ) / 24;
    my $days  = $difference % 7;
    my $weeks = ( $difference - $days ) / 7;

    # better way to do this?
    my $temp = ($weeks) ? "${weeks}w " : q{};
    $temp .= ($days)    ? "${days}d "    : q{};
    $temp .= ($hours)   ? "${hours}h "   : q{};
    $temp .= ($minutes) ? "${minutes}m " : q{};
    $temp .= ($seconds) ? "${seconds}s"  : q{};
    return $temp;
}
thrig
  • 34,333
  • 3
  • 63
  • 84
  • Doesn't really answer the question (since my sleep was already running)—but does avoid it in the future, so still useful. Wonder if there is a terminal countdown alarm utility somewhere... – derobert Oct 05 '16 at 19:08
  • See also `zsh`'s `sched` and the `at` command for another approach that allows querying the time. – Stéphane Chazelas Oct 05 '16 at 19:16
0

Should be easy enough with tqdm python module.

Shows a nice visual progress bar and can be used as a unix pipe.

https://pypi.python.org/pypi/tqdm

The following python snippet counts 100 seconds

import time
from tqdm import tqdm
for i in tqdm(range(100)):
    time.sleep(1)

55%|██████████ | 55/100 [00:55<00:45, 1.00s/it]

Sandeep
  • 323
  • 3
  • 7
  • I don't see how it'd be easy from looking at that page (though I'm not a Python programmer). You seem to have something in mind—please add it to your answer. – derobert Nov 17 '16 at 17:26