I'm new to Bash, apologies in advance.
Set-up
I have a particular end date end which depends on a particular starting date s and a period length p such that, end=s+p.
Problem
I want to execute a command if and only if today's date is before or equal to the end date.
That is: execute command iff date ≤ end.
Code
s='20/09/2017'
p='1'
end=$( date -j -v +"$p"d -f "%d/%m/%Y" "$s")
if [[ date < “$end” ]];
then
echo 'ok'
fi
There are 2 things not the way they should be,
p=1 implies end = '21/09/2017' < date = '26/09/2017', but still I get an ok.
I use date < “$end” but I want date ≤ “$end”
How to correct 1 and 2?
When you format the date like YYYYMMDD you can simply use an alphanumeric comparison:
start=$(date +'%Y%m%d')
end=$(date +'%Y%m%d')
# -gt means greater than - for example.
# check `help test` to get all available operators
if [ $start -gt $end ] ; then ...
First you need to use a date format which is lexicographical, meaning the big units come first. I highly recommend the ISO standard YYYY-MM-DD or YYYYMMDD. Second, <= can be written as -le (less or equal).
Your:
end=$( date -j -v+"$p"d -f "%d/%m/%Y" "$s" )
does not produce what you expect:
echo "$end"
Thu Sep 21 14:40:54 CEST 2017
What you expect can be obtained with:
end=$( date -j -v+"$p"d -f "%d/%m/%Y" "$s" +"%d/%m/%Y" )
Your comparison:
if [[ date < “$end” ]]; then
does not behave as you think. date is not expanded as the result of the invocation of the date command. So, your comparison is a string comparison (< in the [[.]] conditional expression context) between string "date" and string "Thu Sep 21 14:40:54 CEST 2017". You should use something like:
date=$( date )
if [[ "$date" < “$end” ]]; then
instead (but using date as a variable name is probably not a very good idea).
As noticed by the other answers, the date format you chose is not the best for comparisons. So, combining the various fixes with the other wise suggestions, you could try something like:
s='20170920'
p='1'
end=$( date -j -v+"$p"d -f "%Y%m%d" "$s" +"%Y%m%d" )
now=$( date +"%Y%m%d" )
if (( now <= end )); then
echo 'ok'
fi
Note: if (( now <= end )); then is an arithmetic comparison and it should work in this specific case. Note also that it uses now and end, not $now and $end: the ((.)) arithmetic comparison interprets variable names as the integer value stored in the variable. Anyway, using dates, even in YYYYmmdd format, as if they were integers is not that clean. Using UNIX timestamps, which should be OK at least until year 2038, is probably cleaner:
s='20170920'
p='1'
end=$( date -j -v+"$p"d -f "%Y%m%d" "$s" +"%s" )
now=$( date +"%s" )
if (( now <= end )); then
echo 'ok'
fi
because here now and end are number of seconds since the Epoch, that is, true integers. Note, however, that the result could be different from that of the previous solution because you have now a one-second accuracy instead of one-day. Chose which one corresponds to your needs.
Related
I am trying to compare a string converted to date and a file creation time in bash.
#!/bin/bash
test='2020-05-13 08:00'
testConverted=$(date -d "$test" +'%Y %m %d %H:%M')
[ "~/fileToCompare" -nt "$testConverted" ] && echo "yes"
This always returns false no matter what date I put for test. Is the conversion of the date wrong? Is this possible to do?
The way to do this is to convert the date string into Unix Epoch time (seconds since Jan 1, 1970) with the date command and then similarly get the Epoch time modification date of the test file using the stat command and compare using arithmetic evaluation
#!bin/bash
testDate='2020-05-13 08:00'
testFile="$HOME/fileToCompare"
if (( $(date -d "$test" +%s) > $(stat "$testFile" -c %Z) )); then
echo "testDate ($testDate) is newer than $testFile"
fi
In bash on macOS, I would like to write a small script with dates (or any other program that would do) that gives me a list of dates in the format yyyymmdd of every Saturday of a given year and saves it to a variable.
For example, if I wanted to have a list of dates for all Saturdays of the year 1850, it should somehow look like this:
var = [ 18500105, 18500112, 18500119, …, 18501228 ]
with the below code:
list=()
for month in `seq -w 1 12`; do
for day in `seq -w 1 31`; do
list=( $(gdate -d "1850$month$day" '+%A %Y%m%d' | grep 'Saturday' | egrep -o '[[:digit:]]{4}[[:digit:]]{2}[[:digit:]]{2}' | tee /dev/tty) )
done
done
However, the above command does not write anything in the array list although it gives me the right output with tee.
How can I fix these issues?
Modifying Dennis Williamson's answer slightly to suit your requirement and to add the results into the array. Works on the GNU date and not on FreeBSD's version.
#!/usr/bin/env bash
y=1850
for d in {0..6}
do
# Identify the first day of the year that is a Saturday and break out of
# the loop
if (( $(date -d "$y-1-1 + $d day" '+%u') == 6))
then
break
fi
done
array=()
# Loop until the last day of the year, increment 7 days at a
# time and append the results to the array
for ((w = d; w <= $(date -d "$y-12-31" '+%j'); w += 7))
do
array+=( $(date -d "$y-1-1 + $w day" '+%Y%m%d') )
done
Now you can just print the results as
printf '%s\n' "${array[#]}"
To set up the GNU date on MacOS you need to do brew install coreutils and access the command as gdate to distinguish it from the native version provided.
Argh, just realised you need it for MacOS date.
I will leave the answer for others that do not have that restriction, but it will not work for you.
This is not quite what you want, but close:
year=1850
firstsat=$(date -d $year-01-01-$(date -d $year-01-01 +%w)days+6days +%Y%m%d)
parset a 'date -d '$firstsat'+{=$_*=7=}days +%Y%m%d' ::: {0..52}
echo ${a[#]}
It has the bug, that it finds the next 53 Saturdays, and the last of those may not be in current year.
parset is part of GNU Parallel.
I didn't do much error checking, but here's another implementation.
Takes day of week and target year as arguments.
Gets the julian day of the first matching weekday requested -
gets the epoch seconds of noon on that day -
as long as the year matches what was requested, adds that date to the array and adds a week's worth of seconds to the tracking variable.
Lather, rinse, repeat until no longer in that year.
$: typeset -f alldays
alldays () { local dow=$1 year=$2 julian=1;
until [[ "$dow" == "$( date +%a -d $year-01-0$julian )" ]]; do (( julian++ )); done;
es=$( date +%s -d "12pm $year-01-0$julian" );
allhits=( $( while [[ $year == "$( date +%Y -d #$es )" ]]; do date +%Y%m%d -d #$es; (( es+=604800 )); done; ) )
}
$: time alldays Sat 1850
real 0m9.931s
user 0m1.025s
sys 0m6.695s
$: printf "%s\n" "${allhits[#]}"
18500105
18500112
18500119
18500126
18500202
18500209
18500216
18500223
18500302
18500309
18500316
18500323
18500330
18500406
18500413
18500420
18500427
18500504
18500511
18500518
18500525
18500601
18500608
18500615
18500622
18500629
18500706
18500713
18500720
18500727
18500803
18500810
18500817
18500824
18500831
18500907
18500914
18500921
18500928
18501005
18501012
18501019
18501026
18501102
18501109
18501116
18501123
18501130
18501207
18501214
18501221
18501228
I am making an adjustment a bash script of mine to output the ordinal part (st, nd, rd, th) of a day of month. I am trying to adjust it to be used within a date pattern in the date command.
Here is a simple test to determine if day of month from date command is an int:
dayOfMonth=$(date +"%d") && [[ ${dayOfMonth} =~ ^[0-9]+$ ]] && echo ${dayOfMonth} is an int || echo ${dayOfMonth} is NOT an int
Output is as I expect
01 is an int
Now I put that code into a script:
#!/bin/bash
[[ ${1} =~ ^[0-9]+$ ]] && echo ${1} is an int || echo ${1} is NOT an int
Seems OK:
dateTest.sh 12
12 is an int
But when I try to use it within a date command, the script is giving me conflicting output. echo sees 12 but the test is seeing %2
date --date='06/12/2012 07:21:22' +"`dateTest.sh %d`"
12 is NOT an int
To clarify my purpose, the end result of the script will be something like this:
#!/bin/bash
n=$(date +"%d")
if [ $# -ge 1 ] ; then
n=$1
if ! [[ $n =~ ^[0-9]+$ ]] ; then
echo Arg dateInteger [$dateInteger] must be an integer.
exit 1
fi
fi
if [ $n -ge 11 -a $n -le 13 ] ; then
echo "th"
else
case $(( $n%10 )) in
1)
echo st
;;
2)
echo nd
;;
3)
echo rd
;;
*)
echo th
;;
esac
fi
So that I can it like this:
date --date='06/12/2012 07:21:22' +"%A, %d`dateTest.sh %d` of %B %Y, %I:%M:%S %p"
which would output
Tuesday, 12th of June 2012, 07:21:22 AM
Finished script re-write. Thanks to input from #GordonDavisson, I completed my script re-write: http://pastebin.com/xZ1afqqC. Now it either outputs just the ordinal from an integer, or will output a fully formatted date where you can use standard format strings from date with the addition of "%O" for the ordinal.
You're doing things in the wrong order -- you have to turn "%d" into an integer (the day of the month) before passing it to your script, not after. Consider the command:
date --date='06/12/2012 07:21:22' +"`dateTest.sh %d`"
What this does is run dateTest.sh %d, i.e. it passes "%d" as the argument to your script. The script naturally outputs "%d is not an int". This is then used as the format string for the date command, i.e. date --date='06/12/2012 07:21:22' +"%d is not an int". The date command replaces the "%d" part with the day number, and leaves the rest alone, giving "12 is not an int".
In order to make this work, you have to get the day number first, then pass that to your script. Something like this:
dateTest.sh "$(date --date='06/12/2012 07:21:22' +"%d")"
Unfortunately, your end result script also wants a bunch of other date formatting done that can't be passed to the dateTest script. I think in that case it'd be best to do it in stages:
dayWithSuffix="$(dateTest.sh "$(date --date='06/12/2012 07:21:22' +"%d")")"
date --date='06/12/2012 07:21:22' +"%A, $dayWithSuffix of %B %Y, %I:%M:%S %p"
BTW, several general scripting suggestions:
Send error/debug output to stderr, not stdout, so it doesn't get confused with the script's regular output (part of the problem here). For example, echo "${1} is NOT an int" >&2
Speaking of which, put strings that contain variables in double-quotes (as I did in that last example) to avoid weird misparsing of whitespace, wildcards, etc. Your result script, for example, contains echo Arg dateInteger [$dateInteger] must be an integer. -- you probably don't realize that under certain circumstances the [$dateInteger] part will be replaced by a list of filenames.
Finally, use $( ... ) instead of backquotes. In most cases they're equivalent, but the parenthesis version is easier to read and avoids some weird parsing oddities of the contents. Notice how I nested two such expressions in the assignment to dayWithSuffix? That's much trickier to get right with backquotes.
How to validate the date in Unix.
Where the date format is DD-MM-YYYY.
For example if i run one script ./ValidateDate.sh DD-MM-YYYY then it should check that the date is in this format and also that the date exists.
If date is valid and in above format then proceed for next else print message 'Invalid date'
Thanks.
Well... This is a fine can o' worms.
Any shell script that you create may not work on all of the various flavors of Unix/Linux.
The BSD date command (found on OS X and Solaris) does a great job at taking in the date and verifying the format. It requires you to specify your date format, but once you do, it has no problems:
if date -j -f "%d-%m-%Y" "$DATE" 2>&1 > /dev/null # No output
then
echo "ERROR: Date '$DATE' is not a valid format."
exit 2
else
echo "Date is '$DATE'."
fi
Linux and other systems that use GNU's date can also validate the date, but use a different syntax:
date -d "$DATE" 2>&1 /dev/null # With a bit of luck this will work...
if $? -ne 0
then
echo "ERROR: Date '$DATE' is not a valid format."
else
echo "Date is '$DATE'."
fi
I say With a bit of luck because it's up to the date command to figure out your date's format. This normally works, and will work with your YYYY-MM-DD format, but it can be confusing:
Imagine this:
$ DATE="15/6/2014" # It's June 15, 2014
$ date -d "$DATE"
date: invalid date `15/6/2014' # Invalid?
That's because in my timezone, the dd/mm/yyyy format isn't a valid format. In my timezone, the date should be mm/dd/yyyy. To get around this you can use Wintermute's suggestion and format the date into ISO format before using GNU's date format.
A more universal possibility is to use Perl or Python to figure out if the date is correct:
if perl -mTime::Piece -e "Time::Piece->strptime(\"$DATE\", \"%Y-%m-%d\")" 2> /dev/null
then
echo "ERROR: Date '$DATE' is not a valid format."
else
echo "Date is '$DATE'."
fi
This Perl oneliner will return a non-zero error status if $DATE isn't a valid date in %Y-%m-%d format.
You can validate the date with the date utility, but you first have to transform the input into something it can understand. I suggest ISO representation (always). It could, for example, look like this:
#!/bin/sh
PATTERN='\([0-9]\{1,2\}\)-\([0-9]\{1,2\}\)-\([0-9]\+\)'
# if the pattern doesn't match, this will give date an empty string, so it will fail as expected.
if date -d $(echo "$1" | sed -n "/$PATTERN/ { s/$PATTERN/\3-\2-\1/; p }") > /dev/null 2>&1 ; then
echo valid
else
echo invalid
fi
awk way
awk 'split($0,a,"-"){print strftime("%d-%m-%Y",mktime(a[3]" "a[2]" "a[1]" 00 00 00"))==$0?"valid":"not valid"}' <<< "31-12-1992"
It Converts the string to epoch,converts epoch back then checks against the original.
Edit:
Thought i would add after testing this works for dates
FROM 01-01-0
TO 31-12-2147483647
Although a drawback is after you go below the year 1000 you have to remove leading zeros from the year.
You can do this fairly easily if you are willing to break the validation into two steps. First, check that the string is in the right format:
date=$1
[[ $date =~ ([0-9][0-9])-([0-9][0-9])-([0-9]+) ]] || { printf "Invalid date format\n"; exit 1; }
If that test passes, you can extract the day, month, and year fields, then verify that each falls in the correct range.
day=${BASH_REMATCH[1]}
month=${BASH_REMATCH[2]}
year=${BASH_REMATCH[3]}
thirty_one_days='0[1-9]|[12][0--9]|3[01]'
thirty_days='0[1-9]|[12][0--9]|30'
twenty_eight_days='0[1-9]|1[0-9]|2[0-8]'
case $month in
01|03|05|07|08|10|12)
[[ $day =~ $thirty_one_days ]] ;;
04|06|09|11)
[[ $day =~ $thirty_days ]] ;;
02)
# 1-28, but 29 OK in a leap year.
[[ $day =~ $twenty_eight_days ]] ||
(( year % 4 == 0 && $year % 400 == 0 && day == 29 ))
*)
false
esac || { print "Invalid date\n"; exit 1; }
So I'm trying to write a simple script in bash that asks user for input date in
following format (YYYY-dd-mm). Unfortunately I got stuck on first step, which is verifying that input is in correct format. I tried using 'date' with no luck (as it returns actual current date). I'm trying to make this as simple as possible. Thank you for your help!
Using regex:
if [[ $date =~ ^[0-9]{4}-[0-3][0-9]-[0-1][0-9]$ ]]; then
or with bash globs:
if [[ $date == [0-9][0-9][0-9][0-9]-[0-3][0-9]-[0-1][0-9] ]]; then
Please note that this regex will accept a date like 9999-00-19 which is not a correct date. So after you check its possible correctness with this regex you should verify that the numbers are correct.
IFS='-' read -r year day month <<< "$date"
This will put the numbers into $year $day and $month variables.
date -d "$date" +%Y-%m-%d
The latter is the format, the -d allows an input date. If it's wrong it will return an error that can be piped to the bit bucket, if it's correct it will return the date.
The format modifiers can be found in the manpage of date man 1 date. Here an example with an array of 3 dates:
dates=(2012-01-34 2014-01-01 2015-12-24)
for Date in ${dates[#]} ; do
if [ -z "$(date -d $Date 2>/dev/null)" ; then
echo "Date $Date is invalid"
else
echo "Date $Date is valid"
fi
done
Just a note of caution: typing man date into Google while at work can produce some NSFW results ;)