Today I saw these two lines in one backup script that was written in perl:
my $d = (localtime())[6]; $d = $d=~/[067]/ ? 0 : $d % 2 + 1;
Does this look cryptic to you? Probably not. But I wanted to write something and thought that these two lines won’t be that obvious for everyone out there. So I decided to explain exactly what goes on.
Before I start, I have to say that these are not just any two random lines of perl code. These are very useful lines that provide a short and elegant solution to a rather common problem. Read on if you interested.
First I’ll tell you what happens in these two lines and why they were in that backup script. Than I will break them out in pieces and explain each part, so that everyone could understand and appreciate the elegancy.
It is a common task of many backup scripts to determine which backup archives to keep and which to overwrite. You don’t want your disk space cluttered with billions of practically identical copies. One of the most usual approaches is to keep a copy or two from the last couple of days and maybe one older copy from a week or month ago.
The easiest way to go about this problem is use some part of the date on which the backup was made in the name of the file. For example, if you will call you archives something-Monday.zip and something-Tuesday.zip and run your backup script on every day of the week, than you will end up with only seven files, one for each day. And it doesn’t matter for how long the script is executed. It will overwrite your something-Monday.zip once every week, on Monday. And you don’t have to come back to clean up the mess – everything is fully automatic.
Now, if you are to keep two backup copies from the last two days and one backup copy from a week ago, then how would you go about it? One of the simpliest ways would be to keep a weekly copy from some specific day of week, say Sunday, and have two copies something-1.zip and something-2.zip each of which would be overwritten every other day of the week.
Confusing? Here is a step-by-step example. Let’s say that we’ll call our Sunday archive something-0.zip (that’s a zero, not a letter “O”). Our monday archive will be called something-1.zip. And our Tuesday archive will have a name something-2.zip. Now, we already have three archives and we don’t need any more of them. We need to overwrite something with our Wednesday copy. somthing-0.zip from Sunday we’ll leave for later. something-2.zip from Tuesday is way too fresh. something-1.zip is thus the best alternative. So we call our Wednesday archive something-1.zip. On Thursday we overwrite our Tuesday copy – something-2.zip. On Friday we overwirte our Monday copy, which is long gone being overwritten by our Wednesday copy – somthing-1.zip. And on Saturday we overwirte our second archive again – something-2.zip. On Sunday we overwrite the copy from the last Sunday – something-0.zip. And than everything repeats itself and goes on and on. Hopefully forever.
Actually, reviewing our strategy from a corporate business point of view, we can slightly optimize it. Saturday and Sunday are weekend in most of the countries in the world. Noone works a lot on weekends, so there is not much changing in turms of data that is getting backed up. So, we can treat Saturday in the same way as Sunday.
Now, how would we go about programming this logic into a backup script? Well, here comes the beauty of Perl. All that complex logic goes exactly in those two lines of code that I wrote above.
You don’t believe me, right? Well, that’s the truth and I can’t do anything about it. No, I can. I can explain to you how it works. Let’s go.
The first line, to refresh you memory, looks like this:
my $d = (localtime())[6];
This one is easy. In fact, you might have seen something similar in a number of other scripts. The variable $d
(‘d’ is for a ‘day’ or something like that) is defined here. The value is also assigned to it. The value comes from the system function localtime()
. If you never saw a Linux machine in your life, here is a small description of the function for you.
localtime()
analyzes the current time (if none is given as an argument, which is the case here) and returns a list (array) of numeric values, each of which represents a portion of the time. Items of the list are in this order:
0 - seconds 1 - minutes 2 - hour 3 - day of month 4 - month 5 - year 6 - day of week 7 - day of year 8 - day time saving
If you are interested in the details of each, please consult the man localtime
manual for system function documentation or perldoc -f localtime
for perl implementation.
In the script that we are talking about, we’ll need only the six element – day of week. This element of the list will have a numeric value of the current day of week. The values are as follows:
0 - Sunday 1 - Monday 2 - Tuesday 3 - Wednesday 4 - Thursday 5 - Friday 6 - Saturday
On some implementations of localtime()
, Sunday is assigned to seven (7) and not zero (0) like here.
So, in the line of code:
my $d = (localtime())[6];
the meaning is this: execute function localtime()
, take the value in the 6th position of the resulting list and assign it to the variable $d
.
The second line of the script is slightly more complicated:
$d = $d=~/[067]/ ? 0 : $d % 2 + 1;
It seems complex because it consists of several parts. The first part to look at is an if ... then ... else
statement. In Perl, there are several ways to write the if ... then ... else
construct. Each way has its uses. Here, we see this syntax:
test ? true : false
This one is good for short blocks, which is exactly the case. The expression before the question mark(?) is evaluated. If it is true, than the block between the question mark (?) and a colon(:) is evaluated and the result is returned. Otherwise, if the evaluated expression is not true (false), then the block after the colon(:) is evaluated and the result is returned.
In our case, the expression is $d=~/[067]/
. This is a regular expression test. It checks if the value of variable $d
matches digit zero (0) or digit six (6) or digit seven (7). So, this part finds out if the backup script was executed on weekend (remember? 0 and 7 are Sundays while 6 stands for Saturdays).
If the expression is true (that is the script was indeed executed on a weekend), than the block between the question mark and the colon is evaluated and the value returned. The block that I am talking about, looks like this:
0
Which means, that if the script was executed on weekend, than a zero is returned.
On any other day of the week, the expression after the colon is evaluated. The expression looks like this:
$d % 2 + 1
This is a simple arithmeticl expression. The percent sign (%) stands for modulo division. We module divide the value stored in variable $d
by 2. Which is nothing more than seeing if a number is odd or even. If the number can be divided by 2, than 0 will be left. If it can’t than 1 will stay. But we don’t want to return 0, because it is what already returned from another block and we need to separate between the two. So, we simply add 1 to the result of modular division. Even numbers will now return 1, and odd numbers will become 2.
Let’s see all these together again:
$d=~/[067]/ ? 0 : $d % 2 + 1
Together it means the following: if the number stored in variable $d
matches 0, 6, or 7 (the script was executed on weekend), than return 0. Otherwise, see check if the number in $
is odd or even. Return 1 for evens and 2 for odds.
The last missing bit of the second line that we are analysing is:
$d =
Which means that take whatever the result of the expression to the right of equal sign (=) was and save it back to variable $d
Alltogether these two lines produce the following result if executed on every day of the week:
Sunday = 0 Monday = 2 Tuesday = 1 Wednesday = 2 Thursday = 1 Friday = 2 Saturday = 1
All there is left to do now, is incorporate the value of $d
into the resulting backup filename. And that is way too easy…
Update: Fixed regexp as in comments.
strange, but I always thought that expression /067/ will match exactly the string “067” or some string having this inside of it like “906712” and to match one of 0,6 or 7 the expression /(0|6|7)/ or at least /[067]/ should be used.
I need to read on regexp more (or you have mistype) :)
Alex, the secret here is not in the
/067/
, but in=~
. The match will be returned from regexp evaluation and assigned to the variable.Actually, Alex is right:
/067/
does not have the intended effect.It should be
/[067]/
as this simple test shows:Note there is never a 0 result.
Now it works.
You probably didn’t test your program on the weekend yet. :-)
You guys are right. I must have been on drugs for the whole week or something. I couldn’t see the difference between what I have typed in this post and what was in the script itself. The script was working just fine. The problem was here.
Thanks for pointing out.
Let’s divide number, not devide :)
I like assembler and it does not let to confuse with spelling
Also I’d like to stress that localtime() returns an array only in array context, in text context the result is very very different
And guess who is noname if these 2 lines are my invention!
Hi
nonameIgor. :)You can do that like this:
my ( $d ) = map { /[067]/ ? 0 : $_ % 2 + 1 } ( localtime )[6];
There is more than one way to do it. Indeed. :)
You say: “Together it means the following: if the number stored in variable $d matches 0, 6, or 7, than return 0” (btw. if ..then)
But in the very end you list:
Saturday = 1
Well spotted! Thanks! That’s what I get for blogging drunk. :)