66

I have just started learning shell script recently, so I don't know much about it.

I am trying to find example of time based while loop but not having any luck.

I want to run a loop for specific amount of time, let's say 1 hour. So loop runs for an hour and then ends automatically.

Edit: This loop will run continiously without any sleep, so the loop condition should be based on loop's start time and current time, not on sleep.

Mihir
  • 1,156
  • 3
  • 13
  • 22

8 Answers8

123

The best way to do this is using the $SECONDS variable, which has a count of the time that the script (or shell) has been running for. The below sample shows how to run a while loop for 3 seconds.

#! /bin/bash
end=$((SECONDS+3))

while [ $SECONDS -lt $end ]; do
    # Do what you want.
    :
done
bsravanin
  • 1,803
  • 1
  • 14
  • 15
  • 3
    this is supported by bash and probably other bourne shell variants, but i'm not sure it's in the POSIX spec, so you should be clear to specify a hash-bang shell more specific than `#!/bin/sh` (which this answer did, but i'm just pointing it out). – Rob Starling Mar 29 '14 at 18:49
  • 3
    +1 for use of `$SECONDS`; @RobStarling: it is indeed NOT part of POSIX (see my answer for a POSIX-only solution); non-exhaustive survey: `$SECONDS` works in `bash`, `zsh`, `ksh`. In `ksh` it even reports fractional seconds. – mklement0 Mar 29 '14 at 19:31
35

Caveat: All solutions in this answer - except the ksh one - can return up to (but not including) 1 second early, since they're based on an integral-seconds counter that advances based on the real-time (system) clock rather than based on when code execution started.


bash, ksh, zsh solution, using special shell variable $SECONDS:

Slightly simplified version of @bsravanin's answer.

Loosely speaking, $SECONDS contains the number of seconds elapsed so far in a script.

In bash and zsh you get integral seconds advancing by the pulse of the system (real-time) clock - i.e., counting behind the scenes does not truly start at 0(!), but at whatever fraction since the last full time-of-day second the script happened to be started at or the SECONDS variable was reset.

By contrast, ksh operates as one would expect: counting truly starts at 0 when you reset $SECONDS; furthermore, $SECONDS reports fractional seconds in ksh.

Therefore, the only shell in which this solution works reasonably predictably and precisely is ksh. That said, for rough measurements and timeouts it may still be usable in bash and zsh.

Note: The following uses a bash shebang line; simply substituting ksh or zsh for bash will make the script run with these shells, too.

#!/usr/bin/env bash

secs=3600   # Set interval (duration) in seconds.

SECONDS=0   # Reset $SECONDS; counting of seconds will (re)start from 0(-ish).
while (( SECONDS < secs )); do    # Loop until interval has elapsed.
  # ...
done

Solution for POSIX-features-only shells, such as sh (dash) on Ubuntu ($SECONDS is not POSIX-compliant)

Cleaned-up version of @dcpomero's answer.

Uses epoch time returned by date +%s (seconds elapsed since 1 January 1970) and POSIX syntax for the conditional.

Caveat: date +%s itself (specifically, the %s format) is not POSIX-compliant, but it'll work on (at least) Linux, FreeBSD, and OSX.

#!/bin/sh

secs=3600                         # Set interval (duration) in seconds.
endTime=$(( $(date +%s) + secs )) # Calculate end time.

while [ $(date +%s) -lt $endTime ]; do  # Loop until interval has elapsed.
    # ...
done
mklement0
  • 382,024
  • 64
  • 607
  • 775
4

You can use the loop command, available here, like so:

$ loop './do_thing.sh' --for-duration 1h --every 5s

Which will do the your thing every five seconds for one hour.

Rich Jones
  • 1,422
  • 1
  • 15
  • 17
3

You can try this

starttime = `date +%s`
while [ $(( $(date +%s) - 3600 )) -lt $starttime ]; do
done

where 'date +%s' gives the current time in seconds.

Raja Simon
  • 10,126
  • 5
  • 43
  • 74
Ashwini
  • 79
  • 3
1

date +%s will give you the seconds since the epoch, so something like

startTime = `date +%s`
timeSpan = #some number of seconds
endTime = timeSpan + startTime

while (( `date +%s` < endTime )) ; do
    #code
done

You might need some edits, since my bash is rusty

dcpomero
  • 979
  • 1
  • 6
  • 17
1

You can explore the -d option of date.

Below is a shell script snippet to exemplify. It is similar to other answers, but may be more useful in different scenarios.

# set -e to exit if the time provided by argument 1 is not valid for date.
# The variable stop_date will store the seconds since 1970-01-01 00:00:00
# UTC, according to the date specified by -d "$1".
set -e
stop_date=$(date -d "$1" "+%s")
set +e

echo -e "Starting at $(date)"
echo -e "Finishing at $(date -d "$1")"

# Repeat the loop while the current date is less than stop_date
while [ $(date "+%s") -lt ${stop_date} ]; do

    # your commands that will run until stop_date

done

You can then call the script in the many different ways date understands:

$ ./the_script.sh "1 hour 4 minutes 3 seconds"
Starting at Fri Jun  2 10:50:28 BRT 2017
Finishing at Fri Jun  2 11:54:31 BRT 2017

$ ./the_script.sh "tomorrow 8:00am"
Starting at Fri Jun  2 10:50:39 BRT 2017
Finishing at Sat Jun  3 08:00:00 BRT 2017

$ ./the_script.sh "monday 8:00am"
Starting at Fri Jun  2 10:51:25 BRT 2017
Finishing at Mon Jun  5 08:00:00 BRT 2017
Gindri
  • 11
  • 2
1

This is exactly what I was looking for, here is a one line solution based on bsravanin's answer:

end=$((SECONDS+30)); of=$((end-SECONDS)) ; while [ $SECONDS -lt $end ]; do echo $((end-SECONDS)) seconds left of $of ; sleep 1 ; done;

David Maman
  • 71
  • 1
  • 4
0

For a more modern approach...

Bash

declare -ir MAX_SECONDS=5
declare -ir TIMEOUT=$SECONDS+$MAX_SECONDS

while (( $SECONDS < $TIMEOUT )); do
    # foo
done

Korn

typeset -ir MAX_SECONDS=5
typeset -ir TIMEOUT=$SECONDS+$MAX_SECONDS

while (( $SECONDS < $TIMEOUT )); do
    # bar
done
Anthony Rutledge
  • 6,980
  • 2
  • 39
  • 44