Как сравнить две даты в оболочке?

Вопрос или проблема

Как можно сравнить две даты в командной оболочке?

Вот пример того, как я хотел бы это использовать, хотя это не работает как есть:

todate=2013-07-18
cond=2013-07-15

if [ $todate -ge $cond ];
then
    break
fi                           

Как я могу добиться желаемого результата?

Правильный ответ все еще отсутствует:

todate=$(date -d 2013-07-18 +%s)
cond=$(date -d 2014-08-19 +%s)

if [ $todate -ge $cond ];
then
    break
fi  

Обратите внимание, что это требует GNU date.

  • Эквивалентный date синтаксис для BSD date (как в OSX по умолчанию) это
    date -j -f "%F" 2014-08-19 +"%s"
    
  • Если вы хотите сравнить с текущей датой, используйте $EPOCHSECONDS вместо одной из дат.

Можно использовать сравнение строк в стиле ksh для сравнения хронологического порядка [строк в формате год, месяц, день].

date_a=2013-07-18
date_b=2013-07-15

if [[ "$date_a" > "$date_b" ]] ;
then
    echo "break"
fi

К счастью, когда [строки, использующие формат YYYY-MM-DD] сортируются* в алфавитном порядке, они также сортируются* в хронологическом порядке.

(* – отсортированы или сравниваются)

В этом случае ничего сложного не нужно. Ура!

Вам не хватает формата даты для сравнения:

#!/bin/bash

todate=$(date -d 2013-07-18 +"%Y%m%d")  # = 20130718
cond=$(date -d 2013-07-15 +"%Y%m%d")    # = 20130715

if [ $todate -ge $cond ]; #поместите цикл там, где вам это нужно
then
 echo 'yes';
fi

Вам также не хватает циклических структур, как вы планируете получить больше дат?

Это не проблема циклических структур, а проблема типов данных.

Эти даты (todate и cond) являются строками, а не числами, поэтому вы не можете использовать оператор “-ge” команды test. (Помните, что нотация в квадратных скобках эквивалентна команде test.)

Что вы можете сделать, так это использовать другую нотацию для ваших дат, чтобы они были целыми числами. Например:

date +%Y%m%d

вернет целое число, например 20130715 для 15 июля 2013 года. Затем вы можете сравнить ваши даты с помощью “-ge” и эквивалентных операторов.

Обновление: если ваши даты заданы (например, вы читаете их из файла в формате 2013-07-13), то вы можете легко предварительно обработать их с помощью tr.

$ echo "2013-07-15" | tr -d "-"
20130715

Оператор -ge работает только с целыми числами, которыми ваши даты не являются.

Если ваш скрипт на bash или ksh или zsh, вы можете использовать оператор < вместо этого. Этот оператор недоступен в dash или других оболочках, которые не выходят за пределы стандарта POSIX.

if [[ $cond < $todate ]]; then break; fi

В любой оболочке вы можете преобразовать строки в числа, уважая порядок дат, просто удаляя дефисы.

if [ "$(echo "$todate" | tr -d -)" -ge "$(echo "$cond" | tr -d -)" ]; then break; fi

Кроме того, вы можете воспользоваться традицией и использовать утилиту expr.

if expr "$todate" ">=" "$cond" > /dev/null; then break; fi

Поскольку вызов подпрограмм в цикле может быть медленным, вы можете предпочесть преобразование с использованием конструкций обработки строк оболочки.

todate_num=${todate%%-*}${todate#*-}; todate_num=${todate_num%%-*}${todate_num#*-}
cond_num=${cond%%-*}${cond#*-}; cond_num=${cond_num%%-*}${cond_num#*-}
if [ "$todate_num" -ge "$cond_num" ]; then break; fi

Конечно, если вы можете получить даты без дефисов с самого начала, вы сможете сравнить их с помощью -ge.

Существует также этот метод из статьи под названием: Простое вычисление даты и времени в BASH с unix.com.

Эти функции являются вырезкой из скрипта в этой теме!

date2stamp () {
    date --utc --date "$1" +%s
}

dateDiff (){
    case $1 in
        -s)   sec=1;      shift;;
        -m)   sec=60;     shift;;
        -h)   sec=3600;   shift;;
        -d)   sec=86400;  shift;;
        *)    sec=86400;;
    esac
    dte1=$(date2stamp $1)
    dte2=$(date2stamp $2)
    diffSec=$((dte2-dte1))
    if ((diffSec < 0)); then abs=-1; else abs=1; fi
    echo $((diffSec/sec*abs))
}

Использование

# вычислить количество дней между 2 датами
    # -s в секундах | -m в минутах | -h в часах  | -d в днях (по умолчанию)
    dateDiff -s "2006-10-01" "2006-10-31"
    dateDiff -m "2006-10-01" "2006-10-31"
    dateDiff -h "2006-10-01" "2006-10-31"
    dateDiff -d "2006-10-01" "2006-10-31"
    dateDiff  "2006-10-01" "2006-10-31"

Я преобразую строки в временные метки unix (секунды с 1.1.1970 0:0:0).
Их можно легко сравнить

unix_todate=$(date -d "${todate}" "+%s")
unix_cond=$(date -d "${cond}" "+%s")
if [ ${unix_todate} -ge ${unix_cond} ]; then
   echo "over condition"
fi

специфичный ответ

Так как я люблю уменьшать количество fork’ов и позволяет много трюков, вот моя цель:

todate=2013-07-18
cond=2013-07-15

Что ж, теперь:

{ read todate; read cond ;} < <(date -f - +%s <<<"$todate"$'\n'"$cond")

Это повторно заполняет обе переменные $todate и $cond, используя только один fork, с выводом команды date -f -, которая принимает stdin для чтения одной даты за раз.

В конце концов, вы можете выйти из цикла с помощью

((todate>=cond))&&break

Или в качестве функции:

myfunc() {
    local todate cond
    { read todate
      read cond
    } < <(
      date -f - +%s <<<"$1"$'\n'"$2"
    )
    ((todate>=cond))&&return
    printf "%(%a %d %b %Y)T старше чем %(%a %d %b %Y)T...\n" $todate $cond
}

Используя встроенный printf в , который может отображать дату и время с секундами с эпохи (см. man bash😉

Этот скрипт использует только один fork.

Альтернатива с ограниченным количеством fork’ов и функцией чтения даты

Это создаст выделенный подпроцесс (только один fork):

mkfifo /tmp/fifo
exec 99> >(exec stdbuf -i 0 -o 0 date -f - +%s >/tmp/fifo 2>&1)
exec 98</tmp/fifo
rm /tmp/fifo

Поскольку ввод и вывод открыты, запись в fifo может быть удалена.

Функция:

myDate() {
    local var="${@:$#}"
    shift
    echo >&99 "${@:1:$#-1}"
    read -t .01 -u 98 $var
}

Замечание Чтобы избежать бесполезных fork’ов, таких как todate=$(myDate 2013-07-18), переменная должна быть установлена самой функцией. И чтобы разрешить свободный синтаксис (с кавычками или без кавычек для строк даты), имя переменной должно быть последним аргументом.

Затем сравнение дат:

myDate 2013-07-18        todate
myDate Mon Jul 15 2013   cond
(( todate >= cond )) && {
    printf "To: %(%c)T > Cond: %(%c)T\n" $todate $cond
    break
}

может вернуть:

To: Thu Jul 18 00:00:00 2013 > Cond: Mon Jul 15 00:00:00 2013
bash: break: только имеет смысл в цикле `for`, `while` или `until`

если вне цикла.

Или используйте функцию соединителя оболочки:

wget https://github.com/F-Hauri/Connector-bash/raw/master/shell_connector.bash

или

wget https://f-hauri.ch/vrac/shell_connector.sh

(которые не совсем одинаковые: .sh содержит полный тестовый скрипт, если не загружен)

source shell_connector.sh
newConnector /bin/date '-f - +%s' @0 0

myDate 2013-07-18        todate
myDate "Mon Jul 15 2013"   cond
(( todate >= cond )) && {
    printf "To: %(%c)T > Cond: %(%c)T\n" $todate $cond
    break
}

В случае, если вы хотите сравнить даты двух файлов, вы можете использовать следующее:

if [ file1 -nt file2 ]
then
  echo "file1 новее чем file2"
fi
# или +ot для "старше чем"

Смотрите https://superuser.com/questions/188240/how-to-verify-that-file2-is-newer-than-file1-in-bash

Даты являются строками, а не целыми числами; вы не можете сравнивать их с помощью стандартных арифметических операторов.

один из подходов, которые вы можете использовать, если разделитель гарантированно -:

IFS=-
read -ra todate <<<"$todate"
read -ra cond <<<"$cond"
for ((idx=0;idx<=numfields;idx++)); do
  (( todate[idx] > cond[idx] )) && break
done
unset IFS

Это работает с bash 2.05b.0(1)-release и старше.

Вы также можете использовать встроенную функцию mysql для сравнения дат. Она дает результат в ‘днях’.

from_date="2015-01-02"
date_today="2015-03-10"
diff=$(mysql -u${DBUSER} -p${DBPASSWORD} -N -e"SELECT DATEDIFF('${date_today}','${from_date}');")

Просто вставьте значения переменных $DBUSER и $DBPASSWORD в соответствии с вашими.

К вашему сведению: на Mac, похоже, если просто ввести дату, такую как “2017-05-05”, в date, то это добавит текущее время к дате, и вы будете получать разные значения каждый раз, когда вы преобразуете его в эпоху. Чтобы получить более последовательное время эпохи для фиксированных дат, включите фиктивные значения для часов, минут и секунд:

date -j -f "%Y-%m-%d %H:%M:%S" "2017-05-05 00:00:00" +"%s"

Это может быть странностью, ограниченной версией date, которая поставлялась с macOS…. проверено на macOS 10.12.6.

Поддерживая ответ @Gilles (“Оператор -ge работает только с целыми числами”), вот пример.

curr_date=$(date +'%Y-%m-%d')
echo "$curr_date"
2019-06-20

old_date="2019-06-19"
echo "$old_date"
2019-06-19

преобразования целых чисел в эпоху, согласно ответу @mike-q на https://stackoverflow.com/questions/10990949/convert-date-time-string-to-epoch-in-bash :

curr_date_int=$(date -d "${curr_date}" +"%s")
echo "$curr_date_int"
1561014000

old_date_int=$(date -d "${old_date}" +"%s")
echo "$old_date_int"
1560927600

"$curr_date" больше (более поздняя) "$old_date", но это выражение неправильно оценивается как Ложь:

if [[ "$curr_date" -ge "$old_date" ]]; then echo 'foo'; fi

… показано явно, вот так:

if [[ "$curr_date" -ge "$old_date" ]]; then echo 'foo'; else echo 'bar'; fi
bar

Сравнения целых чисел:

if [[ "$curr_date_int" -ge "$old_date_int" ]]; then echo 'foo'; fi
foo

if [[ "$curr_date_int" -gt "$old_date_int" ]]; then echo 'foo'; fi
foo

if [[ "$curr_date_int" -lt "$old_date_int" ]]; then echo 'foo'; fi

if [[ "$curr_date_int" -lt "$old_date_int" ]]; then echo 'foo'; else echo 'bar'; fi
bar

Причина, по которой это сравнение не работает хорошо со строкой даты с дефисом, в том, что оболочка предполагает, что число с ведущим 0 является восьмеричным, в данном случае месяц “07”. Существовало много предложенных решений, но самым быстрым и простым является удалить дефисы. Bash имеет функцию замены строк, что делает это быстро и легко, а затем сравнение можно выполнить в арифметическом выражении:

todate=2013-07-18
cond=2013-07-15

if (( ${todate//-/} > ${cond//-/} ));
then
    echo larger
fi

Кажется, что все или большинство ответов до сих пор либо предполагают, что вы можете указать формат даты (что вопрос не уточняет явно), либо предполагают, что у вас есть функциональная версия команды date или что-то в этом роде.

Иногда, однако, вы не контролируете формат даты (например, дата истечения срока действия SSL-сертификата, когда дата дается в виде сокращения названия месяца) и у вас нет доступа к более продвинутой команде date. Поэтому хотя это решение не является полностью универсальным, оно работает в стандартной оболочке bash на Linux, Solaris и FreeBSD и обрабатывает названия месяцев (значительно заимствовано из нескольких умных ответов на https://stackoverflow.com/questions/15252383/unix-convert-month-name-to-number):

#!/usr/bin/bash

function monthnumber {
    month=$(echo ${1:0:3} | tr '[a-z]' '[A-Z]')
    MONTHS="JANFEBMARAPRMAYJUNJULAUGSEPOCTNOVDEC"
    tmp=${MONTHS%%$month*}
    month=${#tmp}
    monthnumber=$((month/3+1))
    printf "%02d\n" $monthnumber
}

# Или, жертвуя некоторой гибкостью и краткостью, вы получите больше читаемости:
function monthnumber2 {
    case $(echo ${1:0:3} | tr '[a-z]' '[A-Z]') in
        JAN) monthnumber="01" ;;
        FEB) monthnumber="02" ;;
        MAR) monthnumber="03" ;;
        APR) monthnumber="04" ;;
        MAY) monthnumber="05" ;;
        JUN) monthnumber="06" ;;
        JUL) monthnumber="07" ;;
        AUG) monthnumber="08" ;;
        SEP) monthnumber="09" ;;
        OCT) monthnumber="10" ;;
        NOV) monthnumber="11" ;;
        DEC) monthnumber="12" ;;
    esac
    printf "%02d\n" $monthnumber
}

TODAY=$( date "+%Y%m%d" )

echo "GET /" | openssl s_client -connect github.com:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > tmp.pem
cert_expiry_date=$(openssl x509 -in tmp.pem -noout -enddate | cut -d'=' -f2)
month_name=$(echo $cert_expiry_date | cut -d' ' -f1)
month_number=$( monthnumber $month_name )
cert_expiration_datestamp=$( echo $cert_expiry_date | awk "{printf \"%d%02d%02d\",\$4,\"${month_number}\",\$2}" )

echo "сравнить: [ $cert_expiration_datestamp -gt $TODAY ]"
if [ $cert_expiration_datestamp -gt $TODAY ] ; then
    echo "все в порядке, дата истечения сертификата в будущем."
else
    echo "ПРЕДУПРЕЖДЕНИЕ: дата истечения срока сертификата в прошлом."
fi

Ответ или решение

Сравнение двух дат в оболочке Linux

Сравнение дат в оболочке Linux может показаться простой задачей, однако, из-за особенностей работы с форматами данных и различиями в командах, важно понимать, как правильно это сделать. В данной статье мы рассмотрим различные методы для сравнения дат в оболочке, а также некоторые нюансы.

Способы сравнения дат

  1. Использование date для преобразования в метки времени

    Самый надёжный способ сравнения двух дат — преобразование их в метки времени (timestamp) с помощью команды date, которая позволяет легко сравнивать числа. Для этого вы можете воспользоваться следующими командами:

    todate=$(date -d "2013-07-18" +%s)
    cond=$(date -d "2013-07-15" +%s)
    
    if [ "$todate" -ge "$cond" ]; then
       echo "Дата больше или равна"
    fi

    Важно отметить, что данный метод требует использования GNU версии date. На системах BSD (например, в macOS) синтаксис будет несколько изменён:

    todate=$(date -j -f "%Y-%m-%d" "2013-07-18" +%s)
    cond=$(date -j -f "%Y-%m-%d" "2013-07-15" +%s)
  2. Сравнение в формате YYYY-MM-DD

    Даты, записанные в формате YYYY-MM-DD, можно сравнивать как строки при помощи сочетания [[ ... ]]:

    date_a="2013-07-18"
    date_b="2013-07-15"
    
    if [[ "$date_a" > "$date_b" ]]; then
       echo "date_a больше date_b"
    fi

    В этом случае вы получаете простой и быстрый способ сравнения, так как строки в этом формате уже отсортированы по времени.

  3. Числовое преобразование для сравнения

    Если вы предпочитаете использовать числовые сравнения, вы можете удалить дефисы из дат:

    todate="2013-07-18"
    cond="2013-07-15"
    
    if (( ${todate//-/} >= ${cond//-/} )); then
       echo "todate больше или равна cond"
    fi

    Этот метод требует, чтобы данные представляли собой корректные даты в формате YYYY-MM-DD.

  4. Сравнение дат без превращения в метки времени

    Если доступ к командам ограничен, можно выполнить разбор дат по компонентам и сравнить их:

    IFS=- read year_a month_a day_a <<< "$date_a"
    IFS=- read year_b month_b day_b <<< "$date_b"
    
    if (( year_a > year_b )) || { (( year_a == year_b )) && (( month_a > month_b )); } || { (( year_a == year_b && month_a == month_b )) && (( day_a > day_b )); }; then
       echo "date_a более поздняя"
    fi

Заключение

Сравнение дат в Linux требует понимания форматов данных и методов, доступных в вашей среде. Использование команды date для преобразования в метки времени, а также простые строковые сравнения — это два наиболее распространённых подхода. Выбор метода зависит от вашей конкретной ситуации, доступности команд и требований к точности.

Используйте указанные методы для вашего скрипта в оболочке, чтобы достигнуть точного сравнения дат, а также повысить читаемость и поддержку ваших скриптов.

Оцените материал
Добавить комментарий

Капча загружается...