How to Increase a Date Within a Loop in Bash

How to increase a date within a loop in Bash

Spaces are important when using the [ command:

while [ "$dateTale" -lt "$now" ]

Also, you don't use the '$' when assigning to variables (unlike perl or php):

dateTale=$(date -d "$dateTale + 1 day" +%Y%m%d)

How to increment a date in a Bash script

Use the date command's ability to add days to existing dates.

The following:

DATE=2013-05-25

for i in {0..8}
do
NEXT_DATE=$(date +%m-%d-%Y -d "$DATE + $i day")
echo "$NEXT_DATE"
done

produces:

05-25-2013
05-26-2013
05-27-2013
05-28-2013
05-29-2013
05-30-2013
05-31-2013
06-01-2013
06-02-2013

Note, this works well in your case but other date formats such as yyyymmdd may need to include "UTC" in the date string (e.g., date -ud "20130515 UTC + 1 day").

How to loop through dates using Bash?

Using GNU date:

d=2015-01-01
while [ "$d" != 2015-02-20 ]; do
echo $d
d=$(date -I -d "$d + 1 day")

# mac option for d decl (the +1d is equivalent to + 1 day)
# d=$(date -j -v +1d -f "%Y-%m-%d" $d +%Y-%m-%d)
done

Note that because this uses string comparison, it requires full ISO 8601 notation of the edge dates (do not remove leading zeros). To check for valid input data and coerce it to a valid form if possible, you can use date as well:

# slightly malformed input data
input_start=2015-1-1
input_end=2015-2-23

# After this, startdate and enddate will be valid ISO 8601 dates,
# or the script will have aborted when it encountered unparseable data
# such as input_end=abcd
startdate=$(date -I -d "$input_start") || exit -1
enddate=$(date -I -d "$input_end") || exit -1

d="$startdate"
while [ "$d" != "$enddate" ]; do
echo $d
d=$(date -I -d "$d + 1 day")
done

One final addition: To check that $startdate is before $enddate, if you only expect dates between the years 1000 and 9999, you can simply use string comparison like this:

while [[ "$d" < "$enddate" ]]; do

To be on the very safe side beyond the year 10000, when lexicographical comparison breaks down, use

while [ "$(date -d "$d" +%Y%m%d)" -lt "$(date -d "$enddate" +%Y%m%d)" ]; do

The expression $(date -d "$d" +%Y%m%d) converts $d to a numerical form, i.e., 2015-02-23 becomes 20150223, and the idea is that dates in this form can be compared numerically.

Loop over a range of dates. d=$(date -I -d $d + 1 day) not working

I also happen to be using a Mac. Here is an alternative script that increments seconds instead, and emits the normal calendar representation of that:

#!/bin/bash

dsecs=`date '+%s'`
ddate=`date -r $dsecs '+%F'`
while [ "$ddate" != 2019-12-31 ]; do
echo 'run date is: ' $ddate
dsecs=$((dsecs + 86400))
ddate=`date -r $dsecs '+%F'`
done

Output:

run date is:  2019-12-24
run date is: 2019-12-25
run date is: 2019-12-26
run date is: 2019-12-27
run date is: 2019-12-28
run date is: 2019-12-29
run date is: 2019-12-30

You could use $ddate as the input for the programs you want to call from this script.

The above script starts the iteration at today's date; if you need to start the iteration at some other date, just replace the first dsecs= line with the Unix Epoch value of, say, noon on the date on which you want the iteration to start


regarding leap seconds

Constructive feedback is welcome, but the concerns raised in comments on this answer and on the question about this script being "buggy" due to ignoring the matter of leap seconds are misplaced. Here's why:

  1. date does not take leap seconds into account, because Unix time does not take leap seconds into account [1]; adding 86,400 seconds to a Unix Epoch time will always cause date to yield the same time, advanced by one day -- i.e., exactly one day later
  2. POSIX time also does not concern itself with leap seconds; c.f., see the discussion on this SO question, and its references
  3. even if leap seconds were at play here, using 86,400 seconds to advance the clock by one whole day only fails when you start at midnight on a date that is going to have a leap second added; otherwise, 86,400 seconds later is "the next day"
  4. yes, because of leap seconds, adding 86,400 seconds repeatedly does produce a drift (but again, not for date) -- but there have only been 27 leap seconds so far, spaced an average of 20 months apart [2]; thus:

    4.1. it generally takes a span of years to even possibly be affected by this

    4.2. a starting time of a half-minute or more after midnight eliminates the effect of this drift entirely

    4.3. there haven't been any leap seconds since 12/31/2016, so this isn't a concern for dates between then and now

  5. future dates do not (yet) have leap seconds at all -- they are not predictable in the same way leap years are -- so calculating future dates is even less of a concern

So, the operation of date itself makes repeatedly adding 86,400 seconds valid, and even if that weren't the case, the circumstances where leap seconds matter are narrow. As I noted in one of my comments, all real-world engineering is done in the context of the application at hand. In the wide, wide swath of circumstances where leap seconds don't amount to enough of an effect -- or don't exist at all -- a solution that does not take them into account is not buggy ... and also not over-engineered.

[1] https://en.wikipedia.org/wiki/Unix_time

[2] https://en.wikipedia.org/wiki/Leap_second

How to pass a range of dates through the for loop in bash?

Don't do it yourself. Dates are tricky, months have variable number of days, etc. Your script also needs additional handling of years. Let date handle calculations with dates.

for i in {1..7}; do ./scriptname.sh $(date --date="-$i days" "+%Y %m %d"); done

.g the week running from 29th March through 4th April

The way I would do it is to take 29th Match as number of seconds since epoch. Then add half a day so that leap seconds if any are not an isssue. Then just increment it by number of seconds in a day and use date to convert it back to a date.

then=$(date --date='2020/03/29 12:00:00' +%s);
for i in {0..6}; do
./scriptname.sh $(date --date="@$((then + $i * 60 * 60 * 24))" "+%Y %m %d");
done

Alternatively, you can do it without then variable, but you have to remember that the number +0 immediately coming after date is interpreted as timezone.

for i in {0..6}; do ./scriptname.sh $(date --date="2020/03/29 12:00:00 +0 +$i days" '+%Y %m %d'); done

Because I like streaming-like parsing in bash, I would parse the output with xargs:

seq 6 | xargs -I{} date -d '2020/03/29 12:00:00Z +{} days' '+%Y %m %d' | xargs -n3 ./scriptname.sh

Print incremental date using while loop in bash

You should store them as timestamps:

#!/bin/bash

sdate=$(date -d '2014-02-12' +%s)
edate=$(date -d '2014-02-25' +%s)

while [[ sdate -le edate ]]; do
date -d "@$sdate" '+%m-%d-%y'
sdate=$(date -d "$(date -d "@${sdate}" ) + 1 day" +%s)
done

Output:

02-12-14
02-13-14
02-14-14
02-15-14
02-16-14
02-17-14
02-18-14
02-19-14
02-20-14
02-21-14
02-22-14
02-23-14
02-24-14
02-25-14
  • Always prefer [[ ]] over [ ] when it comes to conditional expressions in Bash. (( )) may also be a preference.

  • It requires GNU date. e.g. date --version = date (GNU coreutils) 8.21 ...

  • mm-dd-yy is not a format acceptable by date for input so I used yyyy-mm-dd which is acceptable.

bash loop between two given dates

To process each file between two given date/hours, you can use the following:

#!/usr/bin/bash
#set -x

usage() {
echo 'Usage: loopscript.sh <from> <to>'
echo ' <from> MUST be yyyymmdd.hh or empty, meaning 00000000.00'
echo ' <to> can be shorter and is affected by <from>'
echo ' e.g., 20091026.00 27.01 becomes'
echo ' 20091026.00 20091027.01'
echo ' If empty, it is set to 99999999.99'
echo 'Arguments were:'
echo " '${from}'"
echo " '${to}'"
}

# Check parameters.

from="00000000.00"
to="99999999.99"
if [[ ! -z "$1" ]] ; then
from=$1
fi
if [[ ! -z "$2" ]] ; then
to=$2
fi
## Insert this to default to rest-of-day when first argument
## but no second argument. Basically just sets second
## argument to 23 so it will be transformed to end-of-day.
#if [[ ! -z "$1"]] ; then
# if [[ -z "$2"]] ; then
# to=23
# fi
#fi

if [[ ${#from} -ne 11 || ${#to} -gt 11 ]] ; then
usage
exit 1
fi

# Sneaky code to modify a short "to" based on the start of "from".
# ${#from} is the length of ${from}.
# $((${#from}-${#to})) is the length difference between ${from} and ${to}
# ${from:0:$((${#from}-${#to}))} is the start of ${from} long enough
# to make ${to} the same length.
# ${from:0:$((${#from}-${#to}))}${to} is that with ${to} appended.
# Voila! Easy, no?

if [[ ${#to} -lt ${#from} ]] ; then
to=${from:0:$((${#from}-${#to}))}${to}
fi

# Process all files, checking that they're inside the range.

echo "From ${from} to ${to}"
for file in [0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9].[0-9][0-9].* ; do
if [[ ! ( ${file:0:11} < ${from} || ${file:0:11} > ${to} ) ]] ; then
echo " ${file}"
fi
done

When you create the files 20091026.00.${RANDOM} through 20091028.23.${RANDOM} inclusive, this is a couple of sample runs:

pax> ./loopscript.sh 20091026.07 9
From 20091026.07 to 20091026.09
20091026.07.21772
20091026.08.31390
20091026.09.9214
pax> ./loopscript.sh 20091027.21 28.02
From 20091027.21 to 20091028.02
20091027.21.22582
20091027.22.30063
20091027.23.29437
20091028.00.14744
20091028.01.6827
20091028.02.10366
pax> ./loopscript.sh 00000000.00 99999999.99 # or just leave off the parameters.
20091026.00.25772
20091026.01.25964
20091026.02.21132
20091026.03.3116
20091026.04.6271
20091026.05.14870
20091026.06.28826
: : :
20091028.17.20089
20091028.18.13816
20091028.19.7650
20091028.20.20927
20091028.21.13248
20091028.22.9125
20091028.23.7870

As you can see, the first argument must be of the correct format yyyymmdd.hh. The second argument can be shorter since it inherits the start of the first argument to make it the correct length.

This only attempts to process files that exist (from ls) and of the correct format, not every date/hour within the range. This will be more efficient if you have sparse files (including at the start and the end of the range) since it doesn't need to check that the files exist.

By the way, this is the command that created the test files, if you're interested:

pax> for dt in 20091026 20091027 20091028 ; do
for tm in 00 01 02 ... you get the idea ... 21 22 23 ; do
touch $dt.$tm.$RANDOM
done
done

Please don't type that in verbatim and then complain that it created files like:

20091026.you.12345
20091028.idea.77

I only trimmed down the line so it fits in the code width. :-)

Bash for loop for sequence of dates in calendar

Try something like

#!/bin/bash

start=$(date -d "$1" '+%Y%m%d' 2>/dev/null)
end=$(date -d "$2" '+%Y%m%d' 2>/dev/null)

[[ $start != "" && $end != "" ]] || exit 0
[[ $(date -d "$start" +%s) -le $(date -d "$end" +%s) ]] || exit 0

while :; do
echo "$start"
[[ $start -eq $end ]] && exit 0
start=$(date -d "$start +1days" '+%Y%m%d')
done

e.g.

> ./abovescript 20140226 20140303
20140226
20140227
20140228
20140301
20140302
20140303

Bash loop through date time +%Y-%m-%d %H:%M format

I have used epoc for simplicity here. Should work for your use case:

#!/bin/sh

if [ -z $1 ];then
echo Please pass number of seconds
exit 1
fi

epoc_now=`date "+%s"`

epoc_after_hour=`expr $epoc_now + $1`

while [ "${epoc_after_hour}" -gt "${epoc_now}" ]
do
epoc_now=`expr $epoc_now + 60`
date -d "@$epoc_now"
done


Related Topics



Leave a reply



Submit