Bash validate date - linux

I'm writing a shell script and am confused as to why my date validation code is not working. I tried the following solutions to similar questions I found, but is_valid is always set to 1:
date "+%m/%d/%Y" -d "$1" 2>1 > /dev/null
//or..
date -d "2012-02-29" > /dev/null 2>&1
is_valid=$?
#always set to 1, even when given a valid date
How do I correctly validate the date format? The date should only be valid if in the format MM/DD/YYYY
I also tried this solution: Linux Bash - Date Format but it always rejected the date as well.

The BSD date that ships with Mac OS X doesn't support the -d option (or rather, it uses -d for something entirely different). Either install GNU date, or use the following to validate your input string:
date -f "%Y-%m-%d" -j "2012-02-29" >/dev/null 2>&1
The -f provides the input format, and the -j tells date to simply output the date, not attempt to set the system clock.

I came up with this little function:
function isDateInvalid()
{
date -d "$1" "+%m/%d/%Y" > /dev/null 2>&1
res=$?
echo "$res"
}
isDateInvalid "2012-02-219"
1
isDateInvalid "2012-02-29"
0

for y in {2013..2014}; do
for m in {01..12}; do
for d in {01..31}; do
[[ ! "`date -jf %Y%m%d $y$m$d +%Y%m%d`" = "$y$m$d" ]] && continue
echo $y.$m.$d
done
done
done
if strings match, loop will proceed ...

Related

command output in an array in sh shell

I am trying to take output of an command into array and for this tried following:
#!/bin/sh
cmd=($(date +%s;sleep 5; date +%s))
start_time=$cmd[0]
end_time=$cmd[1]
echo $start_time
#EOF
I was expection echo $start_time to give me start time but it print the following:
1572443382 1572443386[0]
Can't switch to bash shell and have only access to sh
Plain sh has no arrays. You have to cope without arrays. In your case that's easy:
start=$(date +%s)
sleep 5
end=$(date +%s)
echo "start=$start end=$end"
If you really, really want to have everything in one subshell then you have to store the output as a plain string and parse that string to retrieve the individual values. You can think of that one string as an "array" where each line is an array entry. Individual lines can be retrieved using sed (which uses indices starting from 1 instead of 0).
times=$(date +%s; sleep 5; date +%s)
echo "start=$(echo "$times" | sed -n 1p) end=$(echo "$times" | sed -n 2p)"
To store individual lines in variables use subshells:
times=$(date +%s; sleep 5; date +%s)
start=$(echo "$times" | sed -n 1p)
end=$(echo "$times" | sed -n 2p)
echo "start=$start end=$end"
However, if you just want to compute how long sleep 5 took you might as well use time sleep 5 which already does that for you.
The positional parameters are the closest thing sh has to an array:
sh-3.2$ set -- "$(date +%s)"; sleep 5; set -- "$#" "$(date +%s)"
sh-3.2$ start=$1 end=$2; echo "$start -> $end"
1572448562 -> 1572448567
You need to use ${array[index]} syntax to access array elements. So, change your start_time and end_time to as follows.
start_time=${cmd[0]}
end_time=${cmd[1]}
Also change
#!/bin/sh
to
#!/bin/bash

Convert human readable time range to the related amount of seconds

I'm trying to figure out how to implement "smart" time parsing as we know it from the shutdowncommand see manpage in one of my own scripts.
So how could I do these conversions within a bash script?
now -> 1448537350
+30 -> 1448539150
+1h -> 1448540950
+1d -> 1448623750
00:00 -> 1448582400
My current solution is not very nice and just supports the +/-XX format but doesn't really parse the passed in argument and rather just uses the argument for the math:
#!/bin/bash
TIME=$(date "+%s")
let TIME=$TIME$1
echo $TIME
Called this way
./script.sh +3600 #in 1 hour
So is there anything I could reuse to have human readble time ranges as arguments?
Cheers
Like this:
date -d 'now' +%s
date -d '+30min' +%s
date -d '+1hour' +%s
date -d '00:00' +%s
I hope that helps. For further reading I suggest man date.
If external tool can be used, you can try dateutil tool.
Here is an example:
dateadd
A tool to perform date arithmetic (date maths) in the shell. Given a date and a list of durations this will compute new dates. Given a duration and a list of dates this will compute new dates.
$ dateadd 2010-02-02 +4d
=>
2010-02-06
$ dateadd 2010-02-02 +1w
=>
2010-02-09
$ dateadd -1d <<EOF
2001-01-05
2001-01-01
EOF
=>
2001-01-04
2000-12-31
Adding durations to times:
$ dateadd 12:05:00 +10m
=>
12:15:00
and even date-times:
$ dateadd 2012-03-12T12:05:00 -1d4h
=>
2012-03-11T08:05:00
You can check if the argument is human readable with date command itself. If not then you can check whether it is numerical or not using regex. An example is like this:
#!/bin/bash
TIME=$(date "+%s")
if date -d "$1" "+%s" > /dev/null 2>&1; then ## Human Readable Format
d=$(date -d "$1" "+%s")
let TIME=$TIME+$d
echo $TIME
elif [[ $1 =~ ^[+-]+{1}[0-9]+$ ]]; then ## Numerical Value
let TIME=$TIME$1
echo $TIME
else
echo "Parsing Error!"
fi

Accept date as user input in bash script

I want to accept date as input in dd-mm-yyyy format as user input (via read) and then convert it as a date object in shell script. How can I achieve this.
I have had success with yyyy-mm-dd but somehow,it is not working for my use case.
This is my script:-
#!/bin/bash
read -p "Enter a date (dd-mm-yyyy): " user_date
if date=$(date -d "$user_date" +'+%d-%m-%Y'); then
# user date was ok
echo $date
This is my input:-
./2.sh
Enter a date (dd-mm-yyyy): 11-1-2011
Error:
date: invalid date `11-1-2011'
Thanks
To compare two strings (there are no date objects, just the string output of the date command), you need to use either [ or preferably a conditional statement [[ ... ]] .
if [[ $user_date = $(date -d "$user_date" +'+%d-%m-%Y') ]]; then
Of course, there's no guarantee that date can parse the string entered by the user as a date.

Iterating over dates with a bash loop in bash -- for (( ... )) not working correctly

I am trying to create a list of dates. This script below works with simple dates with no spaces.
datestart=20130601
dateend=20130705
for (( date1="$datestart"; date1 != dateend; )); do
date1="$(date --date="$date1 + 1 days" +'%Y%m%d')";
echo $date1;
done
When I use a data string like (which contains WHITE SPACE) datestart="2013-06-01 00:00:00" and a date format like +'Y-%m-%d %H:%M:%S'
datestart="2013-06-01 00:00:00"
dateend="2013-07-05 00:00:00"
for (( date1="$datestart"; date1 != "$dateend"; )); do
date1="$(date --date=""$date1" + 1 days" +'%Y-%d-%m %H:%M:%S')";
echo "$date1";
done
I get the following error:
-bash: ((: date1=2013-06-01 00:00:00: syntax error in expression (error token is "00:00:00")
I think I am NOT quoting my variables correctly. I have twiddled and fiddled, and now I am here. How do I quote the variables correctly in a for loop?
This should work for you:
datestart="2013-06-01 00:00:00"
dateend="2013-07-05 00:00:00"
date1="$datestart"
while [[ "$date1" != "$dateend" ]]; do
date1="$(date -u --date="$date1 tomorrow" '+%Y-%m-%d %H:%M:%S')"
echo "$date1"
done
Working Demo
((...)) is used for arithmetic operations only. Use while loop instead.
No need to use nested quotes for $date
Use tomorrow to get next date
Use correct year-month-date format while assigning next date to date1
No way, bash three-expression for loop expects aritmetic expressions. See this link:
http://wiki.bash-hackers.org/syntax/ccmd/c_for
use a classic "while" loop instead.
Addendum
You can use seconds from epoch to allow arithmetics, like in:
datestart=$(date --date="2013-06-01 00:00:00" +%s)
dateend=$(date --date="2013-07-05 00:00:00" +%s)
for (( date1=$datestart; date1 != $dateend; date1+=86400 )); do
date --date=#$date1
done
but care with days that has not 86400 seconds.
You're right about your quotes being off. You had:
date1="$(date --date=""$date1" + 1 days" +'%Y-%d-%m %H:%M:%S')";
but you're doing in and out of your double quotes in very weird ways with this. A quick solution might be to switch to single quotes, which do not isolate variables if they are inside double quotes:
date1="$(date --date="'$date1' + 1 days" +'%Y-%d-%m %H:%M:%S')";
As for the other part ... let's just review what's on the bash man page:
for (( expr1 ; expr2 ; expr3 )) ; do list ; done
First, the arithmetic expression expr1 is evaluated according to
the rules described below under ARITHMETIC EVALUATION. ...
And if you check the ARITHMETIC EVALUATION section of the man page, you'll see that it does not include the sorts of tests that /bin/test or [[ ... ]] can run. Those are covered in the next section of the man page, CONDITIONAL EXPRESSIONS.
If you want to use a for loop, then #pasaba's suggestion to use epoch seconds is what I'd go with also, in order to stick with arithmetic. Something like this:
#!/bin/bash
datestart="2013-06-01 00:00:00"
dateend="2013-07-05 00:00:00"
e_start=$(date -d "$datestart" '+%s')
e_end=$(date -d "$dateend" '+%s')
for (( date1=$e_start; date1 < $e_end; date1+=86400 )); do
echo -n "$date1 "; date -d "#$date1" '+%Y-%m-%d'
done
To account for leap years and leap seconds and the like, you can put your trust in the Linux date command, and evaluate $date1 in the loop as you originally did:
for (( date1=$e_start; date1 < $e_end; )); do
date1=$(date -d "$(date -d "#$date1") + 1 day" '+%s')
echo -n "$date1 "; date -d "#$date1" '+%Y-%m-%d'
done
The nested date commands are required because Linux's date command doesn't allow you to use relative dates ("+1day" or "tomorrow") when the origin date is specified as an epoch with #. (I'd love to know if I'm wrong about that.)
I realize that your question is tagged "Linux", but I'll note for future searches that this is a non-portable (Linux-only) use of the date command, so if you want this script to run in FreeBSD, NetBSD, OSX, etc, you'll need to review their usage. The following works in FreeBSD:
#!/usr/bin/env bash
datestart="2015-06-01 00:00:00"
dateend="2015-07-05 00:00:00"
e_start="$(date -jf '%Y-%m-%d %T' "$datestart" '+%s')"
e_end="$(date -jf '%Y-%m-%d %T' "$dateend" '+%s')"
for (( date1 = $e_start; date1 < $e_end; )); do
date1=$(date -j -v+1d -f '%s' "$date1" '+%s')
echo -n "$date1 "; date -jf '%s' "$date1" '+%Y-%m-%d'
done

check if argument is a valid date in bash shell

I am writing a bash shell script in Linux, this program will accept a date (mm-dd-yyyy) as a parameter. I am wondering if there is a simply way to check if the date is valid? is there an operator and I can just use test to check?
You can check with date -d "datestring"
So date -d "12/31/2012" is valid, but using hyphens, e.g. date -d "12-31-2012", is not valid for date.
You can also use words: date -d 'yesterday' or date -d '1 week ago' are both valid.
You can extract the day, month, and year values from the input date value MM-DD-YYYY and validate it as the unambiguous (ISO) format YYYY-MM-DD instead (you can validate a DD-MM-YYY formatted date as "correct" using date, e.g. 25-12-2010, but it is not a valid MM-DD-YYY date, hence the need to change the date format first)
A valid date in the correct format is OK
30th November 2005 is valid:
$ DATE=11-30-2005; d=${DATE:3:2}; m=${DATE:0:2}; Y=${DATE:6:4}; echo "year=$Y, month=$m, day=$d"; if date -d "$Y-$m-$d" &> /dev/null; then echo VALID; else echo INVALID; fi
year=2005, month=11, day=30
VALID
$ DATE=11-30-2005; if date -d "${DATE:6:4}-${DATE:0:2}-${DATE:3:2}" &> /dev/null; then echo VALID; else echo INVALID; fi
VALID
An invalid date in the correct format is NOT OK
31st November 2005 does not validate:
$ DATE=11-31-2005; d=${DATE:3:2}; m=${DATE:0:2}; Y=${DATE:6:4}; echo "year=$Y, month=$m, day=$d"; if date -d "$Y-$m-$d" &> /dev/null; then echo VALID; else echo INVALID; fi
year=2005, month=11, day=31
INVALID
$ DATE=11-31-2005; if date -d "${DATE:6:4}-${DATE:0:2}-${DATE:3:2}" &> /dev/null; then echo VALID; else echo INVALID; fi
INVALID
A valid date in the incorrect format is NOT OK
20th April 1979 in DD-MM-YYYY format does not validate as a MM-DD-YYYY date:
$ DATE=20-04-1979; d=${DATE:3:2}; m=${DATE:0:2}; Y=${DATE:6:4}; echo "year=$Y, month=$m, day=$d"; if date -d "$Y-$m-$d" &> /dev/null; then echo VALID; else echo INVALID; fi
year=1979, month=20, day=04
INVALID
$ DATE=20-04-1979; if date -d "${DATE:6:4}-${DATE:0:2}-${DATE:3:2}" &> /dev/null; then echo VALID; else echo INVALID; fi
INVALID
Alternate simpler method: use BASH variable string replace hyphens to slashes
$ DATE="04-30-2005"; [[ $(date -d "${DATE//-/\/}" 2> /dev/null) ]] && echo VALID || echo INVALID
VALID
$ DATE="04-31-2005"; [[ $(date -d "${DATE//-/\/}" 2> /dev/null) ]] && echo VALID || echo INVALID
INVALID
For script use, I kept it as simple as I could. Testing the date value with the date function then checking the exit code of the process.
date -d "02/01/2000" 2>: 1>:; echo $?
This will redirect the standard in and standard error to null : and using echo to return the exit code with $? allows me to check for 0=good date and 1=bad date.
The following worked well for me. Many thanks to my co-worker, Tyler Chamberlain, for the OSX solution.
# Validate a given date/time in Bash on either Linux or Mac (OSX).
# Expected date/time format (in quotes from the command line): YYYY-MM-DD HH:MM:SS
# Example(s): ./this_script "2012-02-29 13:00:00" # IS valid
# ./this_script "2013-02-29 13:00:00" # Is NOT valid
START_DATETIME=$1
function report_error_and_exit
{
local MSG=$1
echo "$MSG" >&2
exit 1
}
# We can use OSTYPE to determine what OS we're running on.
# From http://stackoverflow.com/questions/394230/detect-the-os-from-a-bash-script
# Determine whether the given START_DATETIME is valid.
if [[ "$OSTYPE" == "linux-gnu" ]]
then
# Validate the date on a Linux machine (Redhat or Debian). On Linux, this is
# as easy as adding one minute and checking the return code. If one minute
# cannot be added, then the starting value is not a valid date/time.
date -d "$START_DATETIME UTC + 1 min" +"%F %T" &> /dev/null
test $? -eq 0 || report_error_and_exit "'$START_DATETIME' is not a valid date/time value. $OSTYPE"
elif [[ "$OSTYPE" == "darwin"* ]]
then
# Validate the date on a Mac (OSX). This is done by adding and subtracting
# one minute from the given date/time. If the resulting date/time string is identical
# to the given date/time string, then the given date/time is valid. If not, then the
# given date/time is invalid.
TEST_DATETIME=$(date -v+1M -v-1M -jf "%F %T" "$START_DATETIME" +"%F %T" 2> /dev/null)
if [[ "$TEST_DATETIME" != "$START_DATETIME" ]]
then
report_error_and_exit "'$START_DATETIME' is not a valid date/time value. $OSTYPE"
fi
fi
echo "The date/time is valid."
I tested this script on a Red Hat-based system, a Debian-based system and OSX, and it worked as expected on all three platforms. I did not have time to test on Windows (Cygwin).
For validation of YYYY-MM-DD (ISO 8601) dates on OSX in the BASH shell, the following approach validates both the format and the date.
isYYYYMMDDdate() {
[[ "$1" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] && [[ "$1" == $(date -r $(date -j -f "%Y-%m-%d" "$1" "+%s") '+%Y-%m-%d') ]] &> /dev/null; echo "$?"
}
It first uses a regular expression match to check the format.
Then, it converts the date to epoch time and then back to a date.
If the original and twice-converted dates match, then it is valid.
Test a valid date: 2005-11-30
$ isYYYYMMDDdate 2005-11-30
0
Test an invalid date: 2005-11-31
$ isYYYYMMDDdate 2005-11-31
1
Test a valid date formatted incorrectly: 1979-20-04
$ isYYYYMMDDdate 1979-20-04
1
The date command will parse a date given with the -d argument. If the date is invalid, an error message is printed to STDERR and date exits with an error status. If the date is valid, it prints the date on STDOUT and exits with a success status.
Because of this, date -d "$date" can be used directly in a bash if statement.
The first wrinkle is that to prevent printing a message for valid dates, you need to redirect STDOUT to /dev/null using >/dev/null.
The second wrinkle is that date accepts an empty string as a valid date without complaint. In most cases, that should mean that your user didn't enter a date when they should have. You will want to test for an empty date separately using the test [ "z$date" != "z" ]
date also accepts a variety of formats. If you are using actual bash (as opposed to dash or some of ther sh variety, you could use regular expressions against your preferred format in place of a simple check for an empty string. For example to check my preferred ISO format, I would use: [[ "$date" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]
date=2001-01-01
if [ "z$date" != "z" ] && date -d "$date" >/dev/null
then
echo "VALID DATE"
fi
If you try this with an invalid date (such as 2001-01-53), it doesn't get into the if and it prints out:
date: invalid date ‘2001-01-53’
Alternately, you could check if the date is invalid and exit:
date=2001-01-01
if [ "z$date" == "z" ]
then
echo "No date specified"
exit 1
fi
if ! [[ "$date" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]
then
echo "Expected date in YYYY-MM-DD format"
exit 1
fi
if ! date -d "$date" >/dev/null
then
exit 1
fi
echo "VALID DATE"
case statements make it easy to support multiple formats and capturing date-parts, i.e.
case ${date} in
[0-3][0-9]-[0-1][0-9]-[0-9][0-9][0-9][0-9] )
yr=...
mn=...
dy=...
;;
[0-1][0-9]-[0-3][0-9]-[0-9][0-9][0-9][0-9] )
yr=...
dy=...
mn=...
;;
.... other formats
;;
* )
echo "ERROR on date format, from value=$date, expected formats ..."
return 1
;;
esac
I hope this helps.
You can use the strptime() function available in Python's time or datetime modules or Perl's Time::Piece module.

Resources