Запустить cron-задачу в первый понедельник каждого месяца?

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

Я хотел бы запустить задачу из cron в 8:30 в первый понедельник каждого месяца. Страница Википедии по cron говорит:

Хотя задача обычно выполняется, когда поля спецификации времени/даты совпадают с текущим временем и датой, существует одно исключение: если одновременно ограничены оба поля “день месяца” и “день недели” (не “*”), то либо поле “день месяца” (3), либо поле “день недели” (5) должно совпадать с текущим днем.

(мое выделение)

Это значит, что я не могу сделать первый понедельник месяца, я могу только сделать первый (или любой другой) день месяца? Я не могу придумать выход из этой ситуации.

Вы можете поместить условие в команду crontab (универсальный способ):

[ "$(date '+%u')" = "1" ] && echo "Сегодня понедельник"

если ваша локаль EN/US, вы также можете сравнить строки (начальный ответ):

[ "$(date '+%a')" = "Mon" ] && echo "Сегодня понедельник"

Теперь, если это условие истинно в один из первых семи дней месяца, у вас есть первый понедельник. Обратите внимание, что в crontab синтаксис процентов необходимо экранировать (универсальный способ):

0   12  1-7 *   *   [ "$(date '+\%u')" = "1" ] && echo "Сегодня понедельник"

если ваша локаль EN/US, вы также можете сравнить строки (начальный ответ):

0   12  1-7 *   *   [ "$(date '+\%a')" = "Mon" ] && echo "Сегодня понедельник"

Замените команду echo на фактическую команду, которую хотите выполнить. Я также нашел аналогичный подход.

У меня компьютер с испанской локалью, поэтому этот способ не работает для меня, потому что mon заменяется на lun.

Другие языки тоже будут ошибаться, поэтому я сделал небольшое изменение в принятом ответе, которое убирает языковой барьер:

0 9 1-7 * *   [ "$(date '+\%u')" = "1" ] && echo "¡Es lunes!"

Мне проще, когда нет необходимости обрабатывать номера дней.

Запустить в первый понедельник месяца:

0 2 * * 1 [ `date '+\%m'` == `date '+\%m' -d "1 week ago"` ] || /path/to/command

т.е. если месяц неделю назад не совпадает с текущим месяцем, значит, мы находимся на 1-м дне (1 = понедельник) месяца.

Аналогично, для третьей пятницы:

0 2 * * 6 [ `date '+\%m'` == `date '+\%m' -d "3 weeks ago"` ] || /path/to/command

т.е. если месяц 3 недели назад отличается от текущего месяца, значит, мы находимся на 3-м дне (6 = пятница) месяца.

Существует хитрый способ сделать это с классическим (Vixie, Debian) cron:

30 8 */100,1-7 * MON

Поле дня месяца начинается со звезды (*), и поэтому cron считает его “неограниченным” и использует логику И между полями дня месяца и дня недели.

*/100 означает “каждые 100 дней, начиная с числа 1”. Так как нет месяцев с более чем 100 днями, */100,1-7 фактически означает “в датах с 1 по 7”.

Вот моя статья с более подробной информацией: Запланируйте Cronjob на первый понедельник каждого месяца по необычному способу.

Так как я интерпретирую свои cron-выражения с помощью PHP и JavaScript, я не могу использовать bash. В конце концов, я нашел, что это возможно сделать только с cron:

0 30 8 * 1/1 MON#1

Я запланировал задачу на выполнение в 4:00 PM в четвертый понедельник каждого месяца следующим образом:

0 16 22-28 * Mon [ "$(date '+\%a')" == "Mon" ] && touch /home/me/FourthMonOfMonth.txt

Рекомендую использовать

"$(/bin/date '+%\w')" = "1"

вместо

"$(date '+\%a')" = "Mon"

чтобы избежать проблем с локалью.

Этот ответ расширяет ответ @ChiragPansheriya на такой же вопрос.

кратко

Сделайте быстрый тест вашей реализации cron, вставив это в ваш crontab:

# Для первого понедельника (или вторника и т.д.) месяца, используйте */32,1-7 для дней месяца
# Для второго понедельника (или вторника и т.д.) месяца, используйте */32,8-14 для дней месяца
# Для третьего понедельника (или вторника и т.д.) месяца, используйте */32,15-21 для дней месяца
# Для четвертого понедельника (или вторника и т.д.) месяца, используйте */32,22-28 для дней месяца
# Для пятого понедельника (или вторника и т.д.) месяца, используйте */32,29-31 для дней месяца
# Объяснение: https://superuser.com/a/1813556
* * */32,1-7   * 0  echo "Первое воскресенье" >> cron_out.txt
* * */32,8-14  * 0  echo "Второе воскресенье" >> cron_out.txt
* * */32,15-21 * 0  echo "Третье воскресенье" >> cron_out.txt
* * */32,22-28 * 0  echo "Четвертое воскресенье" >> cron_out.txt
* * */32,29-31 * 0  echo "Пятое воскресенье" >> cron_out.txt
* * */32,1-7   * 1  echo "Первый понедельник" >> cron_out.txt
* * */32,8-14  * 1  echo "Второй понедельник" >> cron_out.txt
* * */32,15-21 * 1  echo "Третий понедельник" >> cron_out.txt
* * */32,22-28 * 1  echo "Четвертый понедельник" >> cron_out.txt
* * */32,29-31 * 1  echo "Пятый понедельник" >> cron_out.txt
* * */32,1-7   * 2  echo "Первый вторник" >> cron_out.txt
* * */32,8-14  * 2  echo "Второй вторник" >> cron_out.txt
* * */32,15-21 * 2  echo "Третий вторник" >> cron_out.txt
* * */32,22-28 * 2  echo "Четвертый вторник" >> cron_out.txt
* * */32,29-31 * 2  echo "Пятый вторник" >> cron_out.txt
* * */32,1-7   * 3  echo "Первое среда" >> cron_out.txt
* * */32,8-14  * 3  echo "Второе среда" >> cron_out.txt
* * */32,15-21 * 3  echo "Третье среда" >> cron_out.txt
* * */32,22-28 * 3  echo "Четвертое среда" >> cron_out.txt
* * */32,29-31 * 3  echo "Пятое среда" >> cron_out.txt
* * */32,1-7   * 4  echo "Первое четверг" >> cron_out.txt
* * */32,8-14  * 4  echo "Второе четверг" >> cron_out.txt
* * */32,15-21 * 4  echo "Третье четверг" >> cron_out.txt
* * */32,22-28 * 4  echo "Четвертое четверг" >> cron_out.txt
* * */32,29-31 * 4  echo "Пятое четверг" >> cron_out.txt
* * */32,1-7   * 5  echo "Первое пятница" >> cron_out.txt
* * */32,8-14  * 5  echo "Второе пятница" >> cron_out.txt
* * */32,15-21 * 5  echo "Третье пятница" >> cron_out.txt
* * */32,22-28 * 5  echo "Четвертое пятница" >> cron_out.txt
* * */32,29-31 * 5  echo "Пятое пятница" >> cron_out.txt
* * */32,1-7   * 6  echo "Первое суббота" >> cron_out.txt
* * */32,8-14  * 6  echo "Второе суббота" >> cron_out.txt
* * */32,15-21 * 6  echo "Третье суббота" >> cron_out.txt
* * */32,22-28 * 6  echo "Четвертое суббота" >> cron_out.txt
* * */32,29-31 * 6  echo "Пятое суббота" >> cron_out.txt

Объяснение

Кто-то может задать вопрос:

Элемент */32 в поле дней месяца всегда ложен (ни один месяц не имеет более 31 дня), зачем его включать?

Оказывается, если первый символ поля дней месяца или дней недели — это звезда (*), cron переключается с логического оператора ИЛИ на оператор И между полями дней месяца и дней недели. Элемент */32 нужен просто для того, чтобы вызвать это изменение поведения.

Это упоминается в странице man для cronie née vixie-cron:

Примечание: Дата выполнения команды может быть указана в следующих двух полях — “день месяца” и “день недели”. Если оба поля ограничены (т.е. не содержат символа “*”), команда будет выполняться, когда совпадает одно из полей. Например, “30 4 1,15 * 5” приведет к выполнению команды в 4:30 утра 1 и 15 числа каждого месяца, плюс в каждую пятницу.
https://www.mankier.com/5/crontab

день месяца или день недели содержат символ *, команда будет выполнена только тогда, когда оба поля совпадают с текущим временем.

Вы можете увидеть, как наличие ведущего символа * в поле дней месяца изменяет интерпретацию crontab.guru:

спецификация записи crontab интерпретация crontab guru
0 8 1-7 * 1 В 08:00 в каждый день месяца с 1 по 7 и в понедельник.
0 8 */32,1-7 * 1 В 08:00 в каждый 32-й день месяца и каждый день месяца с 1 по 7, если это понедельник.

Изучение исходного кода

Фраза, используемая в man-странице, “содержит символ *“, является двусмысленной. Означает ли это, состоять исключительно из символа *? Или это означает, содержать символ * в любой точке поля? Обратясь к исходному коду vixie cron (entry.c), мы видим, что флаги DOM_STAR и DOW_STAR устанавливаются, если первый символ их соответствующих полей — звездочка (*).

/* DOM (дни месяца)
*/

if (ch == '*')
    e->flags |= DOM_STAR;                    ①
ch = get_list(e->dom, FIRST_DOM, LAST_DOM,
              PPC_NULL, ch, file);
if (ch == EOF) {
    ecode = e_dom;
    goto eof;
}

[...]

/* DOW (дни недели)
*/

if (ch == '*')
    e->flags |= DOW_STAR;                    ②
ch = get_list(e->dow, FIRST_DOW, LAST_DOW,
              DowNames, ch, file);
if (ch == EOF) {
    ecode = e_dow;
    goto eof;
}

① Флаг DOM_STAR устанавливается, если первый символ поля дней месяца — это символ *
② Флаг DOW_STAR устанавливается, если первый символ поля дней недели — это символ *

Позже, в cron.c, если любой из этих двух флагов установлен, cron переключает логический оператор между полями дней месяца и дней недели с ИЛИ на И:

/* ситуация с dom/dow странная. ' * * 1,15 * Sun ' будет выполняться в
 * первый и пятнадцатый И в каждую воскресенье;  ' * * * * Sun ' будет выполняться *только*
 * по воскресеньям;  ' * * 1,15 * * ' будет выполняться *только* 1 и 15.  вот почему мы храним ' e->dow_star ' и ' e->dom_star '. да, это странно.
 * как многие странные вещи, это стандарт.
 */
for (u = db->head;  u != NULL;  u = u->next) {
    for (e = u->crontab;  e != NULL;  e = e->next) {
        Debug(DSCH|DEXT, ("user [%s:%d:%d:...] cmd=\"%s\"\n",
                          env_get("LOGNAME", e->envp),
                          e->uid, e->gid, e->cmd))
        if (bit_test(e->minute, minute) &&
                bit_test(e->hour, hour) &&
                bit_test(e->month, month) &&
                ( ((e->flags & DOM_STAR) || (e->flags & DOW_STAR))        ①
                  ? (bit_test(e->dow,dow) && bit_test(e->dom,dom))        ②
                  : (bit_test(e->dow,dow) || bit_test(e->dom,dom)))) {    ③
            if ((doNonWild && !(e->flags & (MIN_STAR|HR_STAR)))
                    || (doWild && (e->flags & (MIN_STAR|HR_STAR))))
                job_add(e, u);
        }
    }
}

① Флаги DOM_STAR и DOW_STAR проверяются
② Логический оператор ИЛИ вызывает объединение полей дней месяца и дней недели
③ Логический оператор И вызывает пересечение полей дней месяца и дней недели

Мы видели, что проверка “содержит символ *, упомянутая в man-странице, на самом деле является проверкой, является ли символ * первым символом поля.

Осторожно: Данное поведение не регламентировано POSIX, насколько я могу судить. (См. https://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html). Ваша реализация cron может отличаться. Я могу подтвердить, что описанная в этом ответе техника работает на cronie (производная vixie-cron). Это также понимает crontab.guru (пример: 0 8 */32,1-7 * 1).

Некоторые сведения о данном поведении включены в файл FEATURES в исходном коде vixie cron:

-- ситуация с dom/dow странная.  '* * 1,15 * Sun' будет выполняться в
    первый и пятнадцатый И в каждую воскресенье;  '* * * * Sun' будет выполняться *только*
    по воскресеньям;  '* * 1,15 * *' будет выполняться *только* 1 и 15.  вот почему мы храним ' e->dow_star ' и ' e->dom_star '.  Я не придумал
    это поведение; так всегда работал cron, но документация не была очень ясной.  Мне говорили, что некоторые cronie AT&T не действуют таким образом и делают более разумное, то есть (по моему мнению) "или"
    различные совпадения полей вместе.  В этом смысле этот cron может не быть полностью аналогичным некоторым родам нетологии AT&T.

Сравнение

Вот сравнение, как выглядят два подхода в crontab:

0 8 1-7 * *       [ "$(date '+\%u')" = "1" ] && echo "Первый понедельник"
0 8 */32,1-7 * 1  echo "Первый понедельник"

(Первая строка показывает подход принятого ответа).

Связанные обсуждения

Поле дня месяца здесь */100,1-7, что означает “каждые 100 дней, начиная с 1-го числа, и также на датах с 1 по 7”. Поскольку нет месяцев с более чем 100 днями, это снова трюк, чтобы сказать “на датах с 1 по 7”, но с ведущей звездой. Из-за звезды cron выполнит команду в датах с 1 по 7, которые также являются понедельниками.

0 22 */100,1-7 * 2

На примере cron guru ниже вы можете увидеть результат и следующую дату запуска.

Насколько я знаю, это невозможно сделать только с crontab, однако можно использовать обертку функции, чтобы выбрать правильный день из записи “первых семи дней месяца”; смотрите это от записи.

Скрипт обертки будет:

#! /usr/bin/ksh
day=$(date +%d)
if ((day <= 7)) ; then
   exec somecommand
fi
exit 1

и вам нужно будет запустить его (предполагая, что он называется wrapper.sh и доступен глобально), используя запись crontab:

0 0 * * 1 wrapper.sh

На Solaris 10 мне пришлось отформатировать условие следующим образом:

[ `date +\%a` = "Sat" ] && echo "Сегодня суббота"

Вы можете попробовать запустить cronjob для первых семи дней месяца и позволить ему выполняться только в понедельник.

30 8 * * 1 [`date +\%d` -le 07] && 

Выше должно сработать для вас.

Я сделал общее решение для подобных проблем, оно работает для первого, второго, третьего….. последнего рабочего дня месяца.

Вы можете использовать это так:

30 06 * * Mon run-if-today 1 "Mon" && echo "Первый понедельник"
30 06 * * Thu run-if-today 3 "Thu" && echo "Третий четверг"
30 06 * * Sun run-if-today L "Sun" && echo "Последнее воскресенье"

Скрипт run-if-today проверяет как день недели, так и желаемый диапазон недельных дат, если оба совпадают, то он возвращает 0, в противном случае 1.

Проверьте код здесь. https://github.com/xr09/cron-last-sunday

Это использование должно быть наиболее универсальным и избегает проблем с локалью.

[ `/bin/date +\%u` -eq 1 ]

первый понедельник месяца в 6:00 будет выглядеть так в /etc/crontab

00 6 1-7 * *    root    [ `/bin/date +\%u` -eq 1 ] && /run/yourjob.sh

crontab 30 8 */27 * 1

В 08:30, [раз в] каждые 27 дней, и по понедельникам (см. генератор выражений crontab)

День месяца не ограничен (является *), поэтому логика или не применяется.

Меньше запусков в месяц (тест дня в расписании cron, а не команда) и имеет успешный код завершения, если команда не завершается неудачей (из-за ||), и (я думаю) легче в понимании:

11  11  *   *   1   [ $(date +\%-d) -gt 7 ] || echo "Сейчас 11:11 в первый понедельник месяца."

т.е. день месяца не был в первых 7, или команда была выполнена.

Последний день месяца может быть:

4   16  *   *   5   [ $(date +\%-d -d +1week) -gt 7 ] || echo "Сейчас 4:04 по пятнице в последний день месяца."

Мне нужно было такое же решение, но я хотел использовать чистый cron. Надеюсь, это улучшает принятый ответ.

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

Таким образом, это должно выполняться: в полдень, в первые 7 дней месяца, в понедельник:

0 12 1-7 * 1

https://crontab.guru/#0_12_1-7_*_1

Я только что столкнулся с этим, и вот что я придумал:

0 11 * * 1#1 - Запуск в 11 утра в первый понедельник каждого месяца
0 11 * * 1#2 - Запуск в 11 утра во второй понедельник каждого месяца
0 11 * * 1#3 - Запуск в 11 утра в третий понедельник каждого месяца

Если вы хотите запускать во вторник вместо этого:

0 11 * * 2#1 - Запуск в 11 утра в первый вторник каждого месяца
0 11 * * 2#2 - Запуск в 11 утра во второй вторник каждого месяца
0 11 * * 2#3 - Запуск в 11 утра в третий вторник каждого месяца

Я верю, что это решает проблему более элегантно:

30 8 1-7 * 1 /run/your/job.sh

0 9 1-7 * 1 * 

Это будет работать для каждого понедельника каждого месяца.

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

Чтобы запланировать выполнение задания на первом понедельнике каждого месяца в 08:30 с помощью cron, важно понимать, как работают поля в crontab. Стандартная запись cron может не обеспечивать требуемую логику, поэтому нужно использовать особые приемы.

Для выполнения задачи на первом понедельнике месяца, можно использовать следующую запись в crontab:

30 8 1-7 * * [ "$(date '+%u')" -eq 1 ] && /path/to/your/command

Пояснение записи:

  1. 30 8 1-7 : Эта часть cron-выражения означает, что задание будет запускаться в 08:30 суток, в дни с 1 по 7 число каждого месяца.
  2. [ "$(date ‘+%u’)" -eq 1 ]: Эта условная команда проверяет, является ли текущий день понедельником (где 1 соответствует понедельнику).
  3. && — это логический оператор, который позволяет выполнять команду только в случае, если предыдущее условие истинно (то есть если текущий день действительно понедельник).
  4. /path/to/your/command: Замените этот путь на команду или скрипт, который вы хотите выполнить.

Таким образом, запланированное задание будет выполняться только в том случае, если ответ на условие верен, а значит, получится желаемый эффект — выполнение задачи только в первый понедельник месяца.

Альтернативный подход

Другой способ обхода этого ограничения — использовать трюк с конструкцией */32,1-7, который заставляет cron интерпретировать условие по-другому. Это выглядит так:

30 8 */32,1-7 * 1 /path/to/your/command

Примечания:

  • Подход с использованием условия в команде является более универсальным, так как он не зависит от реализации cron и работает на большинстве систем, кроме того, может избежать проблем связанных с локализацией.
  • Убедитесь, что команды и пути в crontab имеют правильные права на выполнение, и что используемая команда работает в несессионной среде (например, если вы используете окружение с ограниченными правами).

Следуя этим рекомендациям, вы сможете успешно настроить cron для выполнения заданий на первом понедельнике каждого месяца.

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

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