359

One of the arguments that my script receives is a date in the following format: yyyymmdd.

I want to check if I get a valid date as an input.

How can I do this? I am trying to use a regex like: [0-9]\{\8}

dragon788
  • 3,583
  • 1
  • 40
  • 49
Peter Nijem
  • 3,625
  • 2
  • 12
  • 7
  • Checking if the format is right is easy. But i don't think that you can, in bash (with built-ins), check if the date is valid. – RedX Jan 14 '14 at 11:59

5 Answers5

556

You can use the test construct, [[ ]], along with the regular expression match operator, =~, to check if a string matches a regex pattern (documentation).

For your specific case, you can write:

[[ "$date" =~ ^[0-9]{8}$ ]] && echo "yes"

Or more a accurate test:

[[ "$date" =~ ^[0-9]{4}(0[1-9]|1[0-2])(0[1-9]|[1-2][0-9]|3[0-1])$ ]] && echo "yes"
#             |\______/\______*______/\______*__________*______/|
#             |   |           |                  |              |
#             |   |           |                  |              |
#             | --year--   --month--           --day--          |
#             |          either 01...09      either 01..09      |
#      start of line         or 10,11,12         or 10..29      |
#                                                or 30, 31      |
#                                                          end of line

That is, you can define a regex in Bash matching the format you want. This way you can do:

[[ "$date" =~ ^regex$ ]] && echo "matched" || echo "did not match"

where commands after && are executed if the test is successful, and commands after || are executed if the test is unsuccessful.

Note this is based on the solution by Aleks-Daniel Jakimenko in User input date format verification in bash.


In other shells you can use grep. If your shell is POSIX compliant, do

(echo "$date" | grep -Eq  ^regex$) && echo "matched" || echo "did not match"

In fish, which is not POSIX-compliant, you can do

echo "$date" | grep -Eq "^regex\$"; and echo "matched"; or echo "did not match"

Caveat: These portable grep solutions are not water-proof! For example, they can be tricked by input parameters that contain newlines. The first mentioned bash-specific regex check does not have this issue.

Cristian Ciupitu
  • 20,270
  • 7
  • 50
  • 76
fedorqui
  • 275,237
  • 103
  • 548
  • 598
  • 3
    @Aleks-DanielJakimenko using grep seems to be the best option if you’re using `sh`, `fish` or other less equipped shells. – tomekwi Aug 03 '15 at 17:50
  • 1
    @tomekwi, yes, of course. But this question is tagged [tag:bash], so it is better to give an answer that makes sense in bash. You can always list alternative sh-compatible solutions, but in no way it should be the main answer. – Aleks-Daniel Jakimenko-A. Aug 03 '15 at 23:17
  • 1
    @fedorqui This regular expression allows month or/and day to be zero. For example these invalid dates are considered as valid by the code: 20160015 20161200 . I think the following patter is more accurate ^[0-9]{4}(0[1-9]|1[0-2])(0[1-9]|[1-2][0-9]|3[0-1])$ – thanos.a Nov 22 '16 at 20:53
  • 1
    Easy to understand code is always preferable to "good" code. This has been studied at length. – Florian Heigl Nov 02 '17 at 16:35
  • -1: I think I found a fatal problem with this approach (using `=~`): any string will incorrectly match an empty string. For example `[[ a =~ "$badvar" ]]; echo $?` gives (incorrectly) 0, while `expr match "a" "$badvar" >/dev/null ; echo $?` gives correct result 1. – Penghe Geng Jul 17 '18 at 19:54
  • @PengheGeng my answer is about using regex to compare strings, while you are focusing on the `=~` operator. Also, note I am using the var on the left hand side of the expression. – fedorqui Jul 25 '18 at 10:53
  • The syntax suggested for `fish` does not work in fish. You need to write `cmd1; and cmd2` instead of `cmd1 && cmd2`. While your solution should still be POSIX-compliant, fish isn't, and you might want to indicate this. – Qw3ry Nov 07 '18 at 09:48
  • @Qw3ry I do not have Fish here with me, would you mind clicking [edit] to suggest improvements? I will gladly approve those. Thanks! – fedorqui Nov 07 '18 at 09:59
  • `=~` would not help me match with `"[^:]+", so using `grep` now. – Pysis Apr 28 '19 at 04:45
  • Caveat: this regex is not "smart" about dates -- `20190230` is considered perfectly valid by this regex, despite there never being a 30st day of February. Similarly it will accept the 31st on any month that actually only has 30 days, e.g. `20190431`. – Doktor J Sep 18 '19 at 19:48
  • 1
    Caveat: the solution using `grep` accepts multi-line values where each line contains a date: e.g. `printf '%s\n%s\n' "$date" "$date" | grep …` will be accepted which might not be expected. – joki May 19 '21 at 08:26
  • 1
    wow! it never occured to me that I could use the batch operator (`&&`, I call it that because it turns the l and r values into an atomic batch) like this. It is a bit less readable than an if statement, but it is so space efficient that it is often worth the tradeoff, in my opinion. Thank you. – Nate T Sep 27 '21 at 16:24
  • @NateT it is useful for ternary expressions --> `echo 1 && echo 2 || echo 3` – fedorqui Sep 27 '21 at 18:48
  • @fedorqui'SOstopharming' yeah, it is. I got so worked up over the 'and / if' that I don't even think I made it to the tenary. I wonder if modern ternary expression syntax evolve from this. (i.e. `a && b || c` => `a : b ? c` ) – Nate T Sep 27 '21 at 19:52
  • Is there a way to do the check with `sed`? That would be useful is one is going to check for a pattern, say `{(.+)}`, and if applicable do some substitution (say `\1`), because grep and sed have slightly different conventions. – Erwann Feb 23 '22 at 21:38
  • @Erwann mmm could you provide an example? – fedorqui Feb 24 '22 at 08:29
  • @fedorqui'SOstopharming' one can do `$ echo '{x}' | grep -E '{.}'; echo "$?"` to determine if there was a match. OTOH `echo '{x}' | sed -E 's/\{(.)\}/\1/'; echo "$?"` return `0` whether there is a match or not (such as by replacing `{x}` by `{x`). The way I deal with this is `if [[ $lhs =~ $pattern ]]; then echo "$lhs" | sed -E "s/$pattern/\1/"; else exit 1; fi` – Erwann Feb 24 '22 at 13:45
  • please fix the grep solution to `printf '%s' "$input" | tr $'\n' ' ' | grep -q -E '^...$'`. i use `tr $'\n' ' '` to replace every newline with an illegal character. here i assume that space is an illegal character – milahu Jul 09 '22 at 07:35
  • Just add one example: `if [[ $(type dircolors) =~ ^dircolors\ is\ .* ]]; then echo "Yes"; else echo "No"; fi` – Kason Jul 11 '22 at 11:49
85

In bash version 3 you can use the '=~' operator:

if [[ "$date" =~ ^[0-9]{8}$ ]]; then
    echo "Valid date"
else
    echo "Invalid date"
fi

Reference: http://tldp.org/LDP/abs/html/bashver3.html#REGEXMATCHREF

NOTE: The quoting in the matching operator within the double brackets, [[ ]], is no longer necessary as of Bash version 3.2

Betlista
  • 10,327
  • 13
  • 69
  • 110
aliasav
  • 3,048
  • 4
  • 25
  • 30
46

A good way to test if a string is a correct date is to use the command date:

if date -d "${DATE}" >/dev/null 2>&1
then
  # do what you need to do with your date
else
  echo "${DATE} incorrect date" >&2
  exit 1
fi

from comment: one can use formatting

if [ "2017-01-14" == $(date -d "2017-01-14" '+%Y-%m-%d') ] 
Betlista
  • 10,327
  • 13
  • 69
  • 110
Django Janny
  • 610
  • 8
  • 13
  • 13
    Highly rate your answer as it lets the date function deal with the dates and not the error-prone regexs' – Ali Feb 01 '17 at 01:38
  • 1
    This is good for checking on broad date options, but if you need to verify a specific date format, can it do that? For example if i do `date -d 2017-11-14e` it returns Tue Nov 14 05:00:00 UTC 2017, but that would break my script. – Josiah Nov 17 '17 at 21:41
  • 1
    You could use something like that : if [ "2017-01-14" == $(date -d "2017-01-14" '+%Y-%m-%d') ] It tests if the date is correct and check if the result is the same as your entered data. By the way, be very careful with localized date format (Month-Day-Year vs. Day-Month-Year for instance) – Django Janny Mar 27 '18 at 18:35
  • 1
    Might not work, depending on your locale. American-formatted dates using MM-DD-YYYY won't work anywhere else in the world, using either DD-MM-YYYY (Europe) or YYYY-MM-DD (some places in Asia) – Paul May 03 '18 at 07:59
  • @Paul, what may not work? As written in a comment, one can use formatting options... – Betlista Jan 31 '20 at 15:36
  • @Betlista, for example this fails, for today's date here in Belgium: date -d "12-01-2021" '+%d-%m-%Y', with "date: invalid date" – Paul Jan 12 '21 at 12:27
  • @Paul I confirm that, very strange on `date +%d-%m-%Y` I got `12-01-2021`, but conversion from string is not working here in Prague, good for new question – Betlista Jan 12 '21 at 12:35
6

In addition to other answers of the =~ Bash operator - Extended Regular Expressions (ERE).

This is the syntax used by awk and egrep (or grep -E),
as well as by Bash's [[ ... =~ ... ]] operator.

For example, a function which supports multiple test provided in multiple arguments:

#!/bin/bash

#-----------#
# Functions #
#-----------#

function RT
{
    declare __line;

    for __line in "${@:2}";
    do
        if ! [[ "$__line" =~ $1 ]];
        then
            return 1;
        fi
    done

    return 0;
}

#-----------#
# Main      #
#-----------#

regex_v='^[0-9]*$';
value_1_v='12345';
value_2_v='67890';

if RT "$regex_v" "$value_1_v" "$value_2_v";
then
    printf 'Valid';
else
    printf 'Invalid';
fi

Description

Function RT or Regex Test

# Declare a local variable for a loop.

declare __line;
# Loop for every argument's value except the first - regex rule

for __line in "${@:2}";
# Test the value and return a **non-zero** return code if failed.
# Alternative: if [[ ! "$__line" =~ $1 ]];

if ! [[ "$__line" =~ $1 ]];
# Return a **zero** return code - success.

return 0;

Main code

# Define arguments for the function to test

regex_v='^[0-9]*$'; # Regex rule
value_1_v='12345'; # First value
value_2_v='67890'; # Second value
# A statement which runs the function with specified arguments
# and executes `printf 'Valid';` if succeeded, else - `printf 'Invalid';`

if RT "$regex_v" "$value_v";

It should be possible to point at failed argument, for example, by appending a counter to the loop and printing its value to stderr.

Related

The quotes around the right-hand side of the =~ operator cause it to become a string, rather than a RegularExpression.

Source

Artfaith
  • 1,183
  • 4
  • 19
  • 29
1

Where the usage of a regex can be helpful to determine if the character sequence of a date is correct, it cannot be used easily to determine if the date is valid. The following examples will pass the regular expression, but are all invalid dates: 20180231, 20190229, 20190431

So if you want to validate if your date string (let's call it datestr) is in the correct format, it is best to parse it with date and ask date to convert the string to the correct format. If both strings are identical, you have a valid format and valid date.

if [[ "$datestr" == $(date -d "$datestr" "+%Y%m%d" 2>/dev/null) ]]; then
     echo "Valid date"
else
     echo "Invalid date"
fi
kvantour
  • 25,269
  • 4
  • 47
  • 72