4
\$\begingroup\$

It's sometimes useful to pause until a specified time (but inconvenient to use at or the like), so I wrote a tiny script to wait until a user-specified time point is reached.

I use this when I want to keep the output of preceding and subsequent commands together, but I don't mind blocking the current terminal. Quite often, this would be M-xcompile when I want to wait until after new source code/data have been uploaded, but still have the results in a compilation-mode buffer.

Although it's a very short script, my time at Code Review has taught me that almost any program has something that can be improved, so please make your suggestions!

#!/bin/sh

# Sleep until the specified time

# Accepts any date/time format accepted by 'date'

# Assumes GNU date and GNU sleep

die() {
    echo "$@" >&2
    exit 1
}

usage() {
    echo "Usage: $0 TIME"
}


test $# = 1 || die $(usage)

case "$1" in
    --version)
        echo "sleep_until version 1.0"
        exit 0
        ;;
    --help)
        usage
        exit 0
        ;;
    -*)
        die "Unrecognised option: $1"
        ;;
    *)
        end=$(date -d "$1" +%s.%N)
        now=$(date +%s.%N)
        test ${end%.*} -gt ${now%.*} || die "$1 is in the past!"
        exec sleep $(echo $end $now - p | dc )
        ;;
esac

Notes:

  • We need GNU date for its -d option and a good range of input formats.
  • GNU sleep handles fractional seconds; we could remove .%N from the date formats to work with traditional/POSIX sleep.
  • I've used dc for the arithmetic so we can use plain sh rather than requiring a more heavyweight shell.
\$\endgroup\$
2
\$\begingroup\$

In a program comment it is said that the program

# Accepts any date/time format accepted by 'date'

That information should be printed with the usage help, plus one or two examples. Something like

$ ./sleep_until.sh --help
Usage: ./sleep_until.sh TIME

    TIME can be any date/time as accepted by date(1).

Examples:

    ...    

That'll save me from looking into the source code (and possibly from looking up man date) in order to figure out the correct parameters.


Due to the truncation in

test ${end%.*} -gt ${now%.*} || die "$1 is in the past!"

there is a (albeit small) chance that a valid date is not recognized. This can be avoided by computing the delay first, and then checking it to be positive (e.g. as in How to compare to floating point number in a shell script):

end=$(date -d "$1" +%s.%N)
now=$(date +%s.%N)
delay=$(echo $end - $now | bc)
test 1 -eq $(echo "$delay > 0" | bc) || die "$1 is in the past!"
exec sleep $delay

Note: I'm using bc rather than dc here just because I needed it for the check $delay > 0 and found a bc-based solution first (and then I did not want to use two different tools). Also I am a little bit more familiar with bc. Some criteria are given in the Unix & Linux question How is bc different from dc?, such as “bc is standardised by POSIX and so is probably the more portable of the two.”

\$\endgroup\$
1
\$\begingroup\$

The substitution in the final exec line can be simplified. Instead of making a pipeline with echo, we can provide input to dc as a command argument:

        exec sleep $(dc -e "$end $now -p")

I always recommend setting -e and -u options for shell scripts. So add at the beginning:

set -eu

This is particularly important, to ensure that we fail neatly when the argument is not understood by date.

I think I missed this because I thought that these were Bashisms, but they are actually specified in POSIX.


Consider adding a --verbose option to print the sleep duration (in seconds) and target time - this could be valuable with some of the relative times supported by date. The argument parsing and checking would need to be reworked, as the simple $# = 1 check would no longer be adequate.

Also, accept -h as a synonym for --help.


Improved code

Applying these suggestions and those from the other answer, I have:

#!/bin/sh

# Assumes GNU date and GNU sleep

set -eu

die() {
    echo "$@" >&2
    exit 1
}

usage() {
    cat <<EOF
Usage:  $0 [-v|--verbose] TIME
or  $0 --version

Sleep until the specified time.

    TIME can be any date/time as accepted by date(1).

Examples:
    $0 3:14
    $0 'next Tuesday'
EOF
}

verbose=false

while [ "${1+y}" ]
do
    case "$1" in
        --version)
            echo "sleep_until version 1.1"
            exit 0
            ;;
        -h|--help)
            usage
            exit 0
            ;;
        -v|--verbose)
            verbose=true
            shift
            ;;
        -*)
            die "Unrecognised option: $1"
            ;;
        *)
            test \! "${2+y}" || die "Extra arguments after time"
            end=$(date -d "$1" +%s.%N)
            now=$(date +%s.%N)
            duration=$(dc -e "$end $now -p")
            case "$duration" in
                -*) die "$1 is in the past!";;
            esac
            if $verbose
            then
                printf 'Sleeping for %g seconds until ' $duration
                date -d "$1"
            fi
            exec sleep $duration
            ;;
    esac
done

# If we reach here, we didn't get any non-option argument
die "No time specified"
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.