Вопрос или проблема
Я хотел бы запустить задачу из 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 "Первый понедельник"
(Первая строка показывает подход принятого ответа).
Связанные обсуждения
-
Почему crontab использует ИЛИ, когда указаны оба дня месяца и дня недели?
https://stackoverflow.com/a/51342252 -
Являются ли поля “день месяца” и “день недели” в crontab взаимно исключающими?
https://unix.stackexchange.com/q/602328
Поле дня месяца здесь */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
Я только что столкнулся с этим, и вот что я придумал:
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
Пояснение записи:
- 30 8 1-7 : Эта часть cron-выражения означает, что задание будет запускаться в 08:30 суток, в дни с 1 по 7 число каждого месяца.
- [ "$(date ‘+%u’)" -eq 1 ]: Эта условная команда проверяет, является ли текущий день понедельником (где 1 соответствует понедельнику).
- && — это логический оператор, который позволяет выполнять команду только в случае, если предыдущее условие истинно (то есть если текущий день действительно понедельник).
- /path/to/your/command: Замените этот путь на команду или скрипт, который вы хотите выполнить.
Таким образом, запланированное задание будет выполняться только в том случае, если ответ на условие верен, а значит, получится желаемый эффект — выполнение задачи только в первый понедельник месяца.
Альтернативный подход
Другой способ обхода этого ограничения — использовать трюк с конструкцией */32,1-7
, который заставляет cron интерпретировать условие по-другому. Это выглядит так:
30 8 */32,1-7 * 1 /path/to/your/command
Примечания:
- Подход с использованием условия в команде является более универсальным, так как он не зависит от реализации cron и работает на большинстве систем, кроме того, может избежать проблем связанных с локализацией.
- Убедитесь, что команды и пути в crontab имеют правильные права на выполнение, и что используемая команда работает в несессионной среде (например, если вы используете окружение с ограниченными правами).
Следуя этим рекомендациям, вы сможете успешно настроить cron для выполнения заданий на первом понедельнике каждого месяца.