As we know that each year have the following max day in each month as follows:
Jan - 31 days
Feb - 28 days / 29 days (leap year)
Mar - 31 days
Apr - 30 days
May - 31 days
Jun - 30 days
Jul - 31 days
Aug - 31 days
Sep - 30 days
Oct - 31 days
Nov - 30 days
Dec - 31 days
How to I get bash to return the value (last day of each month) for the current year without using if else or switch or while loop?
my take:
for m in {1..12}; do
date -d "$m/1 + 1 month - 1 day" "+%b - %d days";
done
To explain: for the first iteration when m=1 the -d argument is "1/1 + 1 month - 1 day" and "1/1" is interpreted as Jan 1st. So Jan 1 + 1 month - 1 day is Jan 31. Next iteration "2/1" is Feb 1st, add a month subtract a day to get Feb 28 or 29. And so on.
cat <<EOF
Jan - 31 days
Feb - `date -d "yesterday 3/1" +"%d"` days
Mar - 31 days
Apr - 30 days
May - 31 days
Jun - 30 days
Jul - 31 days
Aug - 31 days
Sep - 30 days
Oct - 31 days
Nov - 30 days
Dec - 31 days
EOF
cal $(date +"%m %Y") |
awk 'NF {DAYS = $NF}; END {print DAYS}'
This uses the standard cal utility to display the specified month, then runs a simple Awk script to pull out just the last day's number.
Assuming you allow "for", then the following in bash
for m in {1..12}; do
echo $(date -d $m/1/1 +%b) - $(date -d "$(($m%12+1))/1 - 1 days" +%d) days
done
produces this
Jan - 31 days
Feb - 29 days
Mar - 31 days
Apr - 30 days
May - 31 days
Jun - 30 days
Jul - 31 days
Aug - 31 days
Sep - 30 days
Oct - 31 days
Nov - 30 days
Dec - 31 days
Note: I removed the need for cal
For those that enjoy trivia:
Number months from 1 to 12 and look at the binary representation in four
bits {b3,b2,b1,b0}. A month has 31 days if and only if b3 differs from b0.
All other months have 30 days except for February.
So with the exception of February this works:
for m in {1..12}; do
echo $(date -d $m/1/1 +%b) - $((30+($m>>3^$m&1))) days
done
Result:
Jan - 31 days
Feb - 30 days (wrong)
Mar - 31 days
Apr - 30 days
May - 31 days
Jun - 30 days
Jul - 31 days
Aug - 31 days
Sep - 30 days
Oct - 31 days
Nov - 30 days
Dec - 31 days
Try using this code
date -d "-$(date +%d) days month" +%Y-%m-%d
Returns the number of days in the month compensating for February changes in leap years without looping or using an if statement
This code tests date to see if Feb 29th of the requested year is valid, if so then it updates the second character in the day offset string. The month argument selects the respective substring and adds the month difference to 28.
function daysin()
{
s="303232332323" # normal year
((!($2%4)&&($2%100||!($2%400)))) && s=313232332323 # leap year
echo $[ ${s:$[$1-1]:1} + 28 ]
}
daysin $1 $2 #daysin [1-12] [YYYY]
On a Mac which features BSD date you can just do:
for i in {2..12}; do date -v1d -v"$i"m -v-1d "+%d"; done
Quick Explanation
-v stands for adjust. We are adjusting the date to:
-v1d stands for first day of the month
-v"$i"m defined the month e.g. (-v2m for Feb)
-v-1d minus one day (so we're getting the last day of the previous month)
"+%d" print the day of the month
for i in {2..12}; do date -v1d -v"$i"m -v-1d "+%d"; done
31
28
31
30
31
30
31
31
30
31
30
You can add year of course. See examples in the manpage (link above).
Contents of script.sh:
#!/bin/bash
begin="-$(date +'%-m') + 2"
end="10+$begin"
for ((i=$begin; i<=$end; i++)); do
echo $(date -d "$i month -$(date +%d) days" | awk '{ printf "%s - %s days", $2, $3 }')
done
Results:
Jan - 31 days
Feb - 29 days
Mar - 31 days
Apr - 30 days
May - 31 days
Jun - 30 days
Jul - 31 days
Aug - 31 days
Sep - 30 days
Oct - 31 days
Nov - 30 days
for m in $(seq 1 12); do cal $(date +"$m %Y") | grep -v "^$" |tail -1|grep -o "..$"; done
iterate from 1 to 12 (for...)
print calendar table for each month (cal...)
remove empty lines from output (grep -v...)
print last number in the table (tail...)
There is no sense, to avoid using cal, because it is required by POSIX, so should be there
A variation for the accepted answer to show the use of "yesterday"
$ for m in {1..12}; do date -d "yesterday $m/1 + 1 month" "+%b - %d days"; done
Jan - 31 days
Feb - 28 days
Mar - 31 days
Apr - 30 days
May - 31 days
Jun - 30 days
Jul - 31 days
Aug - 31 days
Sep - 30 days
Oct - 31 days
Nov - 30 days
Dec - 31 days
How it works?
Show the date of yesterday for the date "month/1" after adding 1 month
I needed this few times, so when in PHP comes with easy in bash is not,
so I used this till throw me error "invalid arithemtic operator"
and even with warrings in spellcheck ( "mt" stands for month, "yr" for year )
last=$(echo $(cal ${mt} ${yr}) | awk '{print $NF}')
so this works fine...
### get last day of month
#
# implement from PHP
# src: https://www.php.net/manual/en/function.cal-days-in-month.php
#
if [ $mt -eq 2 ];then
if [[ $(bc <<< "${yr} % 4") -gt 0 ]];then
last=28
else
if [[ $(bc <<< "${yr} % 100") -gt 0 ]];then
last=29
else
[[ $(bc <<< "${yr} % 400") -gt 0 ]] && last=28 || last=29
fi
fi
else
[[ $(bc <<< "(${mt}-1) % 7 % 2") -gt 0 ]] && last=30 || last=31
fi
Building on patm's answer using BSD date for macOS (patm's answer left out December):
for i in {1..12}; do date -v1m -v1d -v+"$i"m -v-1d "+%b - %d days"; done
Explanation:
-v, when using BSD date, means adjust date to:
-v1m means go to first month (January of current year).
-v1d means go to first day (so now we are in January 1).
-v+"$i"m means go to next month.
-v-1d means subtract one day. This gets the last day of the previous month.
"+%b - %d days" is whatever format you want the output to be in.
This will output all the months of the current year and the number of days in each month. The output below is for the as-of-now current year 2022:
Jan - 31 days
Feb - 28 days
Mar - 31 days
Apr - 30 days
May - 31 days
Jun - 30 days
Jul - 31 days
Aug - 31 days
Sep - 30 days
Oct - 31 days
Nov - 30 days
Dec - 31 days
Related
I'm writing a bash script on a Fedora 27 machine. This script runs a Python program at intervals, displaying occasional progress messages. It's working except for a line that adds an interval to a time stored in a variable.
Does anyone know how to accomplish this?
Here's a minimal version of the script.
#!/usr/bin/env bash
next_run_dat=${1}
echo -n 'Next run will be at: ';echo ${next_run_dat}
now_dat=`date +'%Y-%m-%d %H:%M:%S'`
echo -n 'The time is now: ';echo ${now_dat}
while [[ ${now_dat} < ${next_run_dat} ]]
do
sleep 10
now_dat=`date +'%Y-%m-%d %H:%M:%S'`
echo -n 'The time is now: ';echo ${now_dat}
done
while true
do
echo 'This line represents a run.'
sleep 5
# ==> PROBLEM LINE BELOW <==
next_run_dat=$(date -d "${next_run_dat} + 25 seconds" +'Y-%m-%d %H:%M:%S')
echo -n 'Next run will be at: ';echo ${next_run_dat}
now_dat=`date +'%Y-%m-%d %H:%M:%S'`
echo -n 'The time is now: ';echo ${now_dat}
while [[ ${now_dat} < ${next_run_dat} ]]
do
sleep 5
now_dat=`date +'%Y-%m-%d %H:%M:%S'`
echo -n 'The time is now: ';echo ${now_dat}
done
done
And here's the output from running the minimal version:
$ bash control_bash_minimal.sh '2018-07-23 09:38:00'
Next run will be at: 2018-07-23 09:38:00
The time is now: 2018-07-23 09:37:36
The time is now: 2018-07-23 09:37:46
The time is now: 2018-07-23 09:37:56
The time is now: 2018-07-23 09:38:06
This line represents a run.
date: invalid date ‘2018-07-23 09:38:00 + 25 seconds’
Next run will be at:
The time is now: 2018-07-23 09:38:11
This line represents a run.
Next run will be at: Y-07-23 09:38:41
The time is now: 2018-07-23 09:38:16
^C
Thanks very much in advance for any help with this problem.
The input format uses + for two different purposes: to specify a timezone in a timestamp, and to specify a relative increase to a timestamp. In your case, date is trying to parse 25 seconds as a timezone. If you specify an explicit timezone, then you can add an offset:
$ date -d "2018-07-23 09:38:00"
Mon Jul 23 09:38:00 EDT 2018
$ date -d "2018-07-23 09:38:00 + 25 seconds"
date: invalid date ‘2018-07-23 09:38:00 + 25 seconds’
# I used a minus sign here to make the output match
# the first example...
$ date -d "2018-07-23 09:38:00-0400 + 25 seconds"
Mon Jul 23 09:38:25 EDT 2018
# ... but positive timezone offsets work too
# (Note that date uses the timezone to parse the
# input, but converts the result to your local
# timezone.)
$ date -d "2018-07-23 09:38:00+0100 + 25 seconds"
Mon Jul 23 04:38:25 EDT 2018
There may be a way to disambiguate without adding an explicit timezone that I am unaware of.
note: this is more an extended comment to chepner's answer
From the manual of GNU coreutils:
Combined date and time of day items
The ISO 8601 date and time of day extended format consists of an ISO 8601 date, a ‘T’ character separator, and an ISO 8601 time of day. This format is also recognized if the ‘T’ is replaced by a space.
In this format, the time of day should use 24-hour notation. Fractional seconds are allowed, with either comma or period preceding the fraction. ISO 8601 fractional minutes and hours are not supported. Typically, hosts support nanosecond timestamp resolution; excess precision is silently discarded.
Sadly enough they make no mention of whether or not a time-zone should be included.
Trying to disentangle the source code and the used GNUlib gives me the feeling that chepner is correct. The double usage of the sign brakes the date parser. To be more correct, it assumes that the first number after + or - is a time-zone offset in hours. Normally, time zones have the format +HH:MM or -HH:MM, but a single number implements it as +HH:00. Evidently, the number has to be smaller than or equal to 24. Example:
$ TZ=UTC date -d "2018-07-23T09:38:00 + 9 seconds"
Mon 23 Jul 00:38:01 UTC 2018
$ TZ=UTC date -d "2018-07-23T09:38:00 + 9 2 seconds"
Mon 23 Jul 00:38:02 UTC 2018
Here, the date is assumed to be in UTC+09:00 and converted to UTC and incremented with a single second and in the second case two seconds.
The example of the OP fails because + 25 seconds is assumed to be UTC+25:00, but this is an invalid time zone:
$ date -d "2018-07-23T09:38:00 + 25 seconds"
date: invalid date ‘2018-07-23T09:38:00 + 25 seconds’
So, how can we add relative times without falling into the TZ-trap?
The date parser expects a signed or unsigned number for relative times. Hence we don't really need to add the plus sign and thus we can exploit this for adding time by removing the + sign:
$ date -d "2018-07-23T09:38:00 25 seconds"
Mon 23 Jul 09:38:25 UTC 2018
This however only works when you add relative time and not when you subtract it. But again, we can trick the parser by adding first ZERO seconds or hours or days or whatever to it:
$ date -d "2018-07-23T09:38:00 - 25 seconds"
date: invalid date ‘2018-07-23T09:38:00 - 25 seconds’
$ date -d "2018-07-23T09:38:00 0 hours - 25 seconds"
Mon 23 Jul 09:37:35 UTC 2018
You can also make use of the keywords next and prev:
$ date -d "2018-07-23T09:38:00 next 25 seconds"
Mon 23 Jul 09:38:25 UTC 2018
$ date -d "2018-07-23T09:38:00 prev 25 seconds"
Mon 23 Jul 09:37:35 UTC 2018
If the time zone if not really of importance, simply work in UTC, just add a Z to the end of the string.
$ date -d "2018-07-23T09:38:00Z + 25 seconds"
Mon 23 Jul 09:38:25 UTC 2018
But the easiest of all is to use float-numbers. As time-zones timezones are given as HH:MM a float cannot be interpreted as a time-zone and thus
$ date -d "2018-07-23T09:38:00 + 25.0 seconds"
Mon 23 Jul 09:38:25 UTC 2018
$ date -d "2018-07-23T09:38:00 - 25.0 seconds"
Mon 23 Jul 09:37:35 UTC 2018
You can convert date into EPOCH seconds and add no of seconds using BASH arithmetic like this:
dt='2018-07-23 09:38:00'
newdt=$(date -d "#$(( $(date -d "$dt" +%s) + 25))" +'%Y-%m-%d %H:%M:%S')
echo "$newdt"
2018-07-23 09:38:25
I am trying to write a POSIX compliant script, which will print all months in specified year $3, that have day in $1 (for example Mo, Tu,...) on a same date as $2 (1,2,3,...).
Example:
Input: ./task1.sh Tu 5 2006
Output:
September 2006
Mo Tu We Th Fr Sa Su
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30
December 2006
Mo Tu We Th Fr Sa Su
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
I have written this script:
#!/bin/sh
year=$3
dayInMonth=$2
dayInWeek=$1
index=1
while expr $index '!=' 13 >/dev/null; do
cal -m $index $year| tail -n +2| while read Mo Tu We Th Fr Sa Su ; do
eval theDay='$'$dayInWeek
if [ "$theDay" = "$dayInMonth" ]; then
cal -m $index $year;
fi
done
index=$(expr $index + 1)
done
But there is a problem with reading of third line of cal output. In these lines numbers of days usually don't start at Mo place. How can I parse third line of cal output so the numbers in $Mo, $Tu, $We,... are always correct?
Update: You've added the requirement for a posix conform solution. date -d as used in my answer is not POSIX conform. I'll keep the answer for those who are using GNU/Linux.
Btw, the following command gives you posixly correct the day of week offset of Jan 5, 2006:
cal 01 2006 | awk -v d=5 'NR>2{for(i=1;i<NF;i++){if($i==d){print i;exit}}}'
You need to tinker a little shell script around that.
I would use the date command, like this:
#!/bin/bash
dayofweek="${1}"
day="${2}"
year="${3}"
for m in {01..12} ; do
date=$(LANG=C date -d "${year}-${m}-${day}" +'%a %B')
read d m <<< "${date}"
[ "${d}" = "${dayofweek}" ] && echo "${m}"
done
Results:
$ bash script.sh Thu 05 2006
January
October
It's easier to check dates with the command date.
for month in {1..12}; do
if [[ $(date -d $(printf "%s-%2.2d-%2.2d" "$year" "$month" "$day") "+%a") == "Tue" ]]; then
cal -m $month $year;
fi
done
The script loops over the 12 months and generate a date based on year and day. The date command outputs the day of the in a 3 letters format with +%a.
If you want the day of week in number format, use +%u and == 2 in the if statement.
How to use cal command to add the calendar of next July to the end of the file, for example, myfile, and what day of the week the upcoming Canada Day fall on?
So far I just have this command:
cal July 2017 >> myfile
I feel like I am not doing it correct and I don't know which command to use, to find the day of the week for specific date.
Use this command:
cal 7 2017 >> file
The output is:
July 2017
Su Mo Tu We Th Fr Sa
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
You can find out day of week of a particular day with the GNU date command:
date -d"2017-07-01" # what day of week is Canada Day this year?
=> Sat Jul 1 00:00:00 UTC 2017
If you just want the week day, then:
date -d"2017-07-01" +%A
=> Saturday
You can check more about these commands with man cal or man date.
On a Mac, you could do this:
date -j -vJulm -v1d -v2017y +%A
See more on this post: date command on Mac OS
Linux date utility can understand a lot of strings including for instance:
$ date -d '8:30'
Fri Jan 2 08:30:00 CET 2015
I'm looking for a way to get the next 8:30, thus:
in case it is Fri Jan 2 before 8:30, the result above should be returned;
otherwise it should print Sat Jan 3 08:30:00 CET 2015.
As one can see next 8:30 doesn't result in the correct answer:
$ date -d 'next 8:30'
date: invalid date ‘next 8:30’
Is there a single expression to calculate this?
Handling it in the shell oneself is of course an option, but makes things more complicates because of daylight save time regulation etc.
In case the clock is adapted to daylight save time, next 8:30 should be parsed to 8:30 according to the settings of the next day.
Testcase:
Given it is Fri Jan 2 12:01:01 CET 2015, the result should be:
$ date -d 'next 8:30'
Sat Jan 3 08:30:00 CET 2015
$ date -d 'next 15:30'
Fri Jan 2 15:30:00 CET 2015
Just use something like:
if [[ $(date -d '8:30 today' +%s) -lt $(date +%s) ]] ; then
next830="$(date -d '8:30 tomorrow')"
else
next830="$(date -d '8:30 today')"
fi
The %s format string gives you seconds since the epoch so the if statement is basically:
if 8:30-today is before now:
use 8:30-tomorrow
else
use 8:30-today
I researched and it does not seem to be possible to do so.
What you can probably do is to compare the hour and minute with 830 and print accordingly:
[ $(date '+%H%M') -le 830 ] && date -d '8:30' || date -d '8:30 + 1 day'
In case you want to work with this easily, create a function to do these calculations.
Test
$ [ $(date '+%H%M') -le 830 ] && date '8:30' || date -d '8:30 + 1 day'
Sat Jan 3 08:30:00 CET 2015
I have a tab-delimited text file with three fields: TIMESTAMP, HOST, and STATUS. I need to find if a host was listed as down less than an hour ago. So far, I have this example:
grep "Down" thetextfile.txt | grep "thehostname"
That gives me a little list of all the times that a host was down in the log. Cool. Now I think I just need to get whether the latest TIMESTAMP is less than an hour ago. I am pretty new to Linux and Bash scripting, but in my other work with actual databases, this would be a relatively simple query.
Any ideas? Or is there a much better approach?
Here's an example of the log file:
TIMESTAMP HOST STATUS
Wed Oct 8 12:16:23 EDT 2014 aserver Alive
Wed Oct 8 12:16:23 EDT 2014 anotherserver Down
Thanks!
You can use this BASH script:
#!/bin/bash
# current date-time in seconds (epoch) value
now=$(date '+%s')
while read -r p; do
# ignore 1st row with headers
[[ "$p" == *TIMESTAMP* ]] && continue
# read 3 values in 3 variables t h s
IFS=$'\t' && read t h s <<< "$p"
# convert date string to epoch value
ts=$(date -d "$t" '+%s')
# if date from file is less than 1 hour ago and status is Down then print host name
[[ "$s" == "Down" ]] && (( (now-ts) < 3600 )) && echo "$h"
done < file
I'd use GNU awk:
gawk -v status=Down -v host=anotherserver '
BEGIN {
mo["Jan"]=1; mo["May"]=5; mo["Sep"]=9
mo["Feb"]=2; mo["Jun"]=6; mo["Oct"]=10
mo["Mar"]=3; mo["Jul"]=7; mo["Nov"]=11
mo["Apr"]=4; mo["Aug"]=8; mo["Dec"]=12
}
function elapsed(month, day, time, year) {
gsub(/:/, " ", time)
return systime() - mktime(sprintf("%d %02d %02d %s", year, mo[month], day, time));
}
$NF == status && $(NF-1) == host && elapsed($2,$3,$4,$6) < 3600
' <<DATA
TIMESTAMP HOST STATUS
Wed Oct 8 12:16:23 EDT 2014 aserver Alive
Wed Oct 8 12:16:23 EDT 2014 anotherserver Down
Wed Oct 16 10:16:23 EDT 2014 aserver Alive
Wed Oct 16 10:16:23 EDT 2014 anotherserver Down
Wed Oct 16 10:16:23 EDT 2014 aserver Down
Wed Oct 16 10:16:23 EDT 2014 anotherserver Up
DATA
Wed Oct 16 10:16:23 EDT 2014 anotherserver Down
Current date is Thu Oct 16 10:53:45 EDT 2014