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?

3
  • 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). Oct 5, 2016 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 5, 2016 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. Oct 5, 2016 at 16:37

5 Answers 5

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;
7
  • 1
    0xffp-6d ... Now, that's a test case! But actually GNU sleep takes things like sleep 1h 30m as well...
    – derobert
    Oct 6, 2016 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. Oct 6, 2016 at 20:02
  • @derobert, updated to support several arguments Oct 6, 2016 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 3, 2018 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? Sep 19, 2018 at 0: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))
3
  • 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. Oct 6, 2016 at 8: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 6, 2016 at 16:09
  • Easier with perl Oct 6, 2016 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

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;
}
2
  • 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 5, 2016 at 19:08
  • See also zsh's sched and the at command for another approach that allows querying the time. Oct 5, 2016 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]

1
  • 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, 2016 at 17:26

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .