Time condition loop in shell - shell

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.

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

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

You can try this
starttime = `date +%s`
while [ $(( $(date +%s) - 3600 )) -lt $starttime ]; do
done
where 'date +%s' gives the current time in seconds.

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.

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

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

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;

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

Related

Calculating PowerShell ticks in Bash

I am translating a PowerShell script in Bash.
This is how the ticks for current datetime are obtained in PowerShell:
[System.DateTime]::Now.Ticks;
By following the definition of Ticks, this is how I am trying to approximate the same calculation using the date command in bash:
echo $(($(($(date -u '+%s') - $(date -d "0001-01-01T00:00:00.0000000 UTC" '+%s'))) * 10000000 ))
This is what I got the last time I tried:
$ echo $(($(($(date -u '+%s') - $(date -d "0001-01-01T00:00:00.0000000 UTC" '+%s'))) * 10000000 )) ; pwsh -c "[System.DateTime]::Now.Ticks;"
637707117310000000
637707189324310740
In particular, the first 7 digits are identical, but digits in position 8 and 9 are still too different between the two values.
I calculated that this means there is just a 2 hours difference between the 2 values. But why? It cannot be the timezone, since I specified UTC timezone in both date commands, right? What do you think?
Note: my suspects about the timezone are increasing, since I am currently based in UTC+2 (therefore 2 hours difference from UTC), but how is this possible since I explicitly specified UTC as timezone in the date commands?
Solved it! The problem wasn't in the date commands, it was in the PowerShell command, which was using the +2 Timezone (CEST time). To fix this, I am now using UtcNow instead of Now.
This is what I am getting now:
$ echo $(($(($(date -u '+%s') - $(date -d "0001-01-01T00:00:00.0000000 UTC" '+%s'))) * 10000000 )) ; pwsh -c "[System.DateTime]::UtcNow.Ticks;"
637707132410000000
637707132415874110
As you can see, now all the digits are identical, except for the last 7th digits, since I added zeros on purpose to convert from seconds to ticks, as I am not interested in fractions of seconds (for now) and I consider them negligible.
Alternative way
Another way to make the two values identical (still excluding fractions of seconds), is to remove the -u option in the first date command in order to use the current time zone, and replace UTC with +0200 in the second date command. If I do this, I can leave Now on the PowerShell command (instead of replacing it with UtcNow).
By doing this, I am getting:
$ echo $(($(($(date '+%s') - $(date -d "0001-01-01T00:00:00.0000000 +0200" '+%s'))) * 10000000)) ; pwsh -c "[System.DateTime]::Now.Ticks;"
637707218060000000
637707218067248090
If you also want fractions of seconds
I just understood that if you also need to consider fractions of seconds, then you just need to add the result of date '+%N' (nanoseconds) divided by 100 to the calculation, in any of the two approaches shown above.
Since the result of date '+%N' can have some leading zeros, Bash may think it's an octal value. To avoid this, just prepend 10# to explicitly say it is a decimal value.
For example, taking the second approach shown above (the "alternative way"), now I get:
$ echo $(($(($(date '+%s') - $(date -d "0001-01-01T00:00:00.0000000 +0200" '+%s'))) * 10000000 + $((10#$(date '+%N')/100)) ))
637707225953311420

Want to use a bash for loop to grab descending months

We have a system that would have a cron job that deletes files up to two months ago. I'm trying to write a script to automate this, but I'm fairly new to bash scripting and was wondering if anyone would be able to help. Our files are in %m%Y format and I would be moving them to another directory and then deleting that directory. So for instance since we are in August (082020), I want to move all files up to June (062020) starting this year in Jan (012020).
Here is my script so far, I am basically just trying to print 012020-062020, can anyone let me know if I am on the right track?
#!/bin/bash
MONTHYEAR=$(date +%m%Y)
DELUPTO=$(expr $(date +%m%Y) - 20000)
CURRENTYEAR=$(date +%Y)
for (( i=$DELUPTO; i>=01 + $CURRENTYEAR; $(expr $i - 10000) ))
do
echo "$i"
done
You should loop from the format yyyydd, so start with
for (( i=202006; i>=202001; i-- )); do
echo "${i:4:2}${i:0:4}"
done
It is up to you how you want to achieve this:
yearmonth=$(date +%Y%m)
or
MONTHYEAR=$(date +%m%Y)
yearmonth=${MONTHYEAR:2:4}${MONTHYEAR:0:2}
You know the month and year, extract those values and then turn it into a stamp, but you will need to insert a day value so I would make it say the 1st:
Example of converting timestamps:
# date -d "8/1/2020" +"%s"
1596254400
# date -d #1596254400 +"%b %d %Y %H:%M:%S"
Aug 01 2020 00:00:00
Then create a time stamp of now minutes X days:
date +%s -d "60 days ago"
Once you have common values to compare, Then compare them and if less than 60 days delete Pseudo code:
del_date=$(date +%s -d "60 days ago")
for each file in directory:
#get month and day from file name here, then
file_date=$(date -d "${fmonth}/1/${fyear}" +"%s")
if [[ $file_date -lt $del_date ]] ;then
echo "Older than 60 days by name"
fi
done
Note: It would probably be better to delete files by checking their ages in the system using stat command opposed to reading the details of file name.

Script to run command at exactly X seconds past each minute

I'd like to run a bash script, for use on a Raspberry Pi, that says "if the seconds of the current time is exactly 00 or 30, then do X".
I've googled and found some suggestions to use cron, but I think there'd be a small delay at the start which I want to avoid.
Thanks
If you don't like the delay of cron, which is mostly for background stuff, you could loop in the foreground:
while true; do
d=$(date +%S)
if [ $d -eq 0 -o $d -eq 30 ]; then
# command here
date +%S.%N
# replace the above command with whatever you want
sleep 5
else
sleep 0.001
fi
done
The Linux date command can check the current system clock quite quickly. I've used this loop to print the nanosecond timer with data to demonstrate the low latency. That is, on my system, I get:
30.001057483
00.003022980
30.003011572
and so on.

Simultaneous commands

I have a script:
#!/bin/bash
date +%T &
Hours=`date +"%H"` &
Minutes=`date +"%M"` &
Seconds=`date +"%S"`
echo "$Hours:$Minutes:$Seconds"
The objective is to echo date two times and then take out hours, minutes, seconds and calculate how many seconds elapsed between those two commands. So my solution is to write hours, minutes, seconds into variables, then work with those variables.
Problem: echo only echoes seconds which means my interpretation of & is wrong.
How can I fix the & problem? I need those commands to run simultaneously so I can check.
date +%s apparently won't work on certain inputs like:
Wed Mar 4 10:34:59 2015
Wed Mar 4 10:35:08 2015
Will give result of 00:00:01 instead of 00:00:09 or:
Wed Mar 4 10:34:59 2015
Wed Mar 4 17:43:08 2015
will give the result of 12:13:14 instead of 07:08:09. Is it true? Or can I use date +%s and then decrease those two outputs?
You don't need to run the commands simultaneously. Run just one command:
read hours minutes seconds < <( date '+%H %M %S' )
But it can be even simpler: just use the +%s format to get number of seconds since the epoch. You'll get two numbers you can safely subtract.
#!/bin/bash
start=$(date +%s)
sleep 10
end=$(date +%s)
echo The command took $(( end - start )) seconds.
The easiest way is to use the shell variable $SECONDS.
Each time this parameter is referenced, the number of seconds since shell invocation is returned. If a value is assigned to SECONDS, the value returned upon
subsequent references is the number of seconds since the assignment plus the
value assigned. If SECONDS is unset, it loses its special properties, even if
it is subsequently reset.
the main problem is that in this case the command date will be executed tree times to get the values..
so you don't need to execute the commad date too many times
#!/bin/bash
#date +%T &
function myTime(){
now=`date +"%H:%M:%S" &`
}
myTime
s1=`echo $now | cut -d":" -f2`
myTime
s2=`echo $now | cut -d":" -f2`
echo "s1[$s1] - s2[$s2]"
Then you can apply your rules to verify the time elapsed
Regards
Claudio

Using a different date in a shell script

SO, I have a whole script that runs every hour with a date as input. Normally, it takes the current time, but now I need it to run for an interval of time in the past, every hour as well.
What I've done so far is:
DEFINING THE OLD DATE
8 start_date=20131218
9 num_hours=5
10 for i in `seq 1 $num_hours`
11 do
12 date=`date -d "${start_date}+${i} hours"`
13 echo $date # Use this however you want!
14
.
.
.
25 done
The starting date is Dec 18, 2013 and then in each iteration it should give me one more hours from the starting time. This part I found it in another article here and it works. The problem comes when I do
echo $(date)
it prints the current time instead of the time that I previously defined. Of course any other variable that I define from the date has the values from the current time. For instance,
18 datestamp=$(date +%F)
19 hourstamp=$(date +%H)
I'm new in shell programming and I have no idea what to do. Any help?
Thanks in advance.
What you want is this:
18 datestamp=$(date -d "${start_date}+${i} hours" +%F)
19 hourstamp=$(date -d "${start_date}+${i} hours" +%H)
As #BMW said, try to avoid use the date as a variable name to avoid ambiguity.
$(date) will run the command date and export the result.
so when echo it, it will return current date/time.
Second, date is the unix command, avoid to use it as a variable. so this will fix your issue"
DATE=`date -d "${start_date}+${i} hours"`
echo $DATE

Resources