Shell script fo announcements for different week days

Good morning all,
I am in need to run different outputs based on next month first Wednesday or second Tuesday or each quarter otherwise run xxx
In short
if (Today +3 = First Saturday) or ( Today +2 = First Saturday) or (Today +1 = First Saturday) then run xxxx followed by
if (Today +3 = Second Tuesday of quarter) or ( Today +2 = Second Tuesday of quarter) or (Today +1 = Second Tuesday of quarter) then run yyyyy
Else run zzzzzz

Drawing a total blank on how to accomplish this

Is this based around cron, like your previous question ?

I don't understand what you are asking for: "based on next month first Wednesday". You can't run something "next Wednesday" -- it runs right now, when you do the test.

For example, 1st May 2024 is Wednesday, so the first Saturday in May is the 4th. Which Wednesday do you want to run the script on -- 1st May or 24th April ?

Same for the second Tuesday of a quarter.

You also say both "otherwise run xxx" and "else run zzzzzz". Both cannot be correct.

my interpretation is the requester wants/needs to do date math ... but wants 'us' to do/show them ... as you remark @Paul_Pedant - looks much like their previous ... and i don't see any efforts on the requestor's part (i did some basics for the first part but stopped as no effort from the requester == no efforts from me)

Quite so. I also note the or conditions each imply the script runs for three consecutive "Todays". I suspect if the requirement was stated coherently (or with correct examples), the solution would be obvious.

Nevertheless, this kind of requirement is common enough: I would have expected by now that crontab would have allowed negative day_of_month, so -1 for last day, 2,-2 for second day and second-to-last day etc. But then I would want last Friday of month etc too.

I would create a calendar text file containing two fields, date and job to run.
Then create a cron job that calculates today's date and then greps the calendar to determine what to run. The calendar only has to contain the days that a job is to be run.

I am never keen on that technique. For me, it contains two issues.

(1) It is impossible to verify your calendar file, except visually. If you have a typo anywhere, it will simply run on the wrong day, or not at all. If it is programmed (and properly tested), it will work forever.

Testing the timing of cron jobs can be tricky. I usually set up to run every five or ten minutes, and to merely append to a log the date/time, and what task and args it would have run live. Then I can edit the script between runs to change the day numbers etc to exercise the logic fully. This avoids the issue of getting one test shot per month on your cron job.

(2) If you make a calendar file, it will only cover a certain period. It cannot (for example) cover multiple years, because the day-of-week and leap years rotate in predictable but unpleasant ways. The longer the period you cover, the more likely it is that the need to refresh the calendar will be overlooked.

As a lifetime contractor, I can never rely on any organisation continuing to do this kind of maintenance stuff. I can always rely on them emailing me fifteen months after I end my time, saying my system has not produced the expected reports for the last three months, and ordering me to show up the next day and fix the system for free, including producing the missing reports when they no longer have the inputs to do so.

The following should do your logic.

#!/bin/bash
is_1st_sat_of_mon(){
  dow=$1 dom=$2
  (( dow == 6 )) && (( dom >= 1 && dom <= 7 ))
}

is_2nd_tue_of_qua(){
  dow=$1 dom=$2 mon=$3
  (( dow == 2 )) && (( dom >= 8 && dom <= 14 )) && (( mon % 3 == 1 ))
}
 
read dow1 dom1 mon1 <<< $( date --date "+ 1 days" "+%w %_d %_m" )
read dow2 dom2 mon2 <<< $( date --date "+ 2 days" "+%w %_d %_m" )
read dow3 dom3 mon3 <<< $( date --date "+ 3 days" "+%w %_d %_m" )
xxxx=0 yyyy=0
if is_1st_sat_of_mon $dow1 $dom1 || is_1st_sat_of_mon $dow2 $dom2 || is_1st_sat_of_mon $dow3 $dom3
then
  echo "xxxx"
  xxxx=1
fi
if is_2nd_tue_of_qua $dow1 $dom1 $mon1 || is_2nd_tue_of_qua $dow2 $dom2 $mon2 || is_2nd_tue_of_qua $dow3 $dom3 $mon3
then
  echo "yyyy"
  yyyy=1
fi
if (( xxxx == 0 && yyyy == 0 ))
then
  echo "zzzz"
fi

Thank you MadeInGermany for your input and suggestion, greatly appreciate it

Yes this is a continuation to my previous quest to accomplish this in crontab, which turned out to be not a good idea.

Just realizing, Saturday and Tuesday are more than 2 days apart from each other, so the respective conditions are exclusive, and the main part can be optimized:

read dow1 dom1 mon1 <<< $( date --date "+ 1 days" "+%w %_d %_m" )
read dow2 dom2 mon2 <<< $( date --date "+ 2 days" "+%w %_d %_m" )
read dow3 dom3 mon3 <<< $( date --date "+ 3 days" "+%w %_d %_m" )
if is_1st_sat_of_mon $dow1 $dom1 || is_1st_sat_of_mon $dow2 $dom2 || is_1st_sat_of_mon $dow3 $dom3
then
  echo "xxxx"
elif is_2nd_tue_of_qua $dow1 $dom1 $mon1 || is_2nd_tue_of_qua $dow2 $dom2 $mon2 || is_2nd_tue_of_qua $dow3 $dom3 $mon3
then
  echo "yyyy"
else
  echo "zzzz"
fi

Also corrected my previous version: 14 not 15.
The 1st week is day 1 - 7, the 2nd week is day 8 - 14

Presumably you intend to initiate this script from a crontab, though. You just need to have cron manage the simple stuff (like the minute, and hour), and have the script do nothing when none of the conditions are met. It is very cheap to have cron call a script that does nothing on most days, and more reliable than pushing the boundaries of crontab.

You can often optimise on the days too. For example, if you want to run on the last Friday of the month you can restrict the days in crontab. The last Friday in February (28 days) can only fall on the 22nd to the 28th. So you can tell cron the day range 22-31 (to accommodate the seven long months as well). The subsequent script check needs to deal with the case where there are two Fridays in that ten-day range, but it does not have to be called for the first 21 days of any month.

I understand the code but I can't comprehend the logic.

In this month (March 2024) the first Saturday is the 2nd.

So on Wed 28th Feb your dow3 check will run the script. On Thu 29th Feb, dow2 will pull the trigger. On Fri 1st March, dow1 will have its say. The quarterly version will do the same. The xxxx and yyyy scripts get run on three consecutive days which can span over two different months, or all be in the month preceding Sat 1st, or all be in the same month as the first Saturday if it is the 4th or later.

That seems somewhat illogical: I can't envisage how that business case could ever arise.

I don't think the original post has enough clarity to be sure this is actually the requirement. I don't understand the initial sentence, and I suspect the "in short" version is logically flawed.

This kind of thing can be exhaustively tested. All you need is a local version of date.x which fakes its output from a test calendar with a bunch of cases.

OK, so figuring out the quarter script execution was easier then I though by simply using crontab
Sharing here so others can benefit by my finding. Chose to use contab to lower the CPU over head as the original idea of having a bash script do all the work would have to be invoked every 5 min to make it useful (see below hourly run for more details).
What I haven;t figured out yet is how and many have shared in lots of post that finding the end of the month is problematic and I have yet to master a) find end of month then check what day of the week it is and set script to run on Wednesday to first Saturday.....will figure it out.

#Board Meeting
#30 1,2,3,4,5,6,8,10,12,14,16,17,19,21,22,23 8-14 1,4,7,10 1-2 /usr/local/MyScript.sh

Translated 30 min past hour (many hours) on 8-14 day of 1st, 4th, 7th, 10th month on second Monday-Tuesday run my script announcing......

Thanks to all who provided useful input and posts to help me get here

A

You can check systemd-timer

See examples here from always good Arch docs (kudoz folks :slight_smile: )
But should apply to most of linux distros which are using systemd.
https://wiki.archlinux.org/title/systemd/Timers

It has more options to manipulate stuff mentioned in thread so perhaps it will help you.

Regards
Peasant.

2 Likes

if inclined, you could change the hours
from
1,2,3,4,5,6,8,10,12,14,16,17,19,21,22,23
to
1-6,8,10,12,14,16,17,19,21-23

#.. Finding the date of the last day of the current month.

$ #.. Get the current year and month.
$ read -r dY dM <<<"$( printf '%(%Y %m)T' -1 )"
$ declare -p dY dM 
declare -- dY="2024"
declare -- dM="03"
$ #.. Jump to the middle of next month (to avoid day steps).
$ read -r dY dM <<<"$( date -d "$dY-$dM-10 + 1 month" '+%Y %m' )"
$ declare -p dY dM 
declare -- dY="2024"
declare -- dM="04"
$ #.. Step back 1 day from the first of that month.
$ read -r EndMth <<<"$( date -d "$dY-$dM-01 - 1 day" '+%F' )"
$ declare -p EndMth
declare -- EndMth="2024-03-31"

$ #.. Check it works over a year end.
$ read -r dY dM <<<"$( echo 2023 12 )"
$ read -r dY dM <<<"$( date -d "$dY-$dM-10 + 1 month" '+%Y %m' )"
$ read -r EndMth <<<"$( date -d "$dY-$dM-01 - 1 day" '+%F' )"
$ declare -p EndMth
declare -- EndMth="2023-12-31"

$ #.. Check it works on a leap year.
$ read -r dY dM <<<"$( echo 2024 02 )"
$ read -r dY dM <<<"$( date -d "$dY-$dM-10 + 1 month" '+%Y %m' )"
$ read -r EndMth <<<"$( date -d "$dY-$dM-01 - 1 day" '+%F' )"
$ declare -p EndMth
declare -- EndMth="2024-02-29"
$

Note on "Days steps": If you tell date "2023-01-30 + 1 month", it will get to 2023-02-30, which does not exist. It folds over to 2023-03-02, and you miss February entirely. So backing one day into the previous month gets you 28-Feb, not 31-Jan as required.

Or you can even try 1-5,6-16/2,17-21/2,22,23

Well, not the complete date, but "the last day":
cal | awk '/[^[:blank:]]/ {print $NF}' | tail -n 1
and I'm sure even this can be shortened.

EDIT: but also...

date -d "$(date +%Y%m01) +1 month -1 day" +%Y-%m-%d
1 Like

triple distilled :smiley:

If using ksh93, then you can use strong builtin date properties.
You can look more examples, fpmurphy blog. (epoch, epoch with nanosecs and so on ...).

# today 2024-04-03
# default is today or use attribute "now" which return current time
# 1st day of month
printf "1st day last month %(%Y-%m-%d)T\n" "last month"
# 2024-03-01
# is same as
printf "1st day last month %(%Y-%m-%d)T\n" "now,last month"
# 2024-03-01
printf "1st day -10 month %(%Y-%m-%d)T\n" "last 10 month"
# 2023-06-01
printf "1st day this month %(%Y-%m-%d)T\n" "month"
# 2024-04-01
printf "1st day next month %(%Y-%m-%d)T\n" "next month"
# 2024-05-01

# last day of month
printf "last day last month %(%Y-%m-%d)T\n" "final last month"
# 2024-03-31
printf "last day this month %(%Y-%m-%d)T\n" "final month"
# 2024-04-30
printf "last day next month %(%Y-%m-%d)T\n" "final next month"
# 2024-05-31
printf "last day -10 month %(%Y-%m-%d)T\n" "final last 10 month"
# 2023-06-30
printf "last day +10 month %(%Y-%m-%d)T\n" "final next 10 month"
# 2025-02-28

# same using some day, ex. 2024-02-24
day="2024-02-24"
# 1st day of month
printf "1st day last month $day = %(%Y-%m-%d)T\n" "$day,last month"
#  2024-01-01
# last day
printf "last day, this month $day = %(%Y-%m-%d)T\n" "$day,final month"
# month 2024-02-29
# next N month last day
printf "last day, next 1 month $day = %(%Y-%m-%d)T\n" "$day,final next 1 month"
# 2024-03-31

# more ...
printf  "3rd wednesday july 2022 %(%Y-%m-%d)T\n" "3rd wednesday july 2022"
# 2022-07-20
printf "next wednesday %(%F)T\n" "next wednesday"
# 2024-04-10