The power of .Net objects in PowerShell just ruined a perfectly good article about the power of .Net objects in PowerShell.
I was writing along, minding my own business, extemporizing on the utility of .Net objects and the discoverability of PowerShell cmdlets and scripting tricks, when the tricks I was demonstrating discovered a better way of accomplishing my scripting goal than the carefully planned solution I was pretending to discover.
The fake goal was to have a script wait until 7 PM before accomplishing some task, changing all of the icons on a coworker's desktop, or whatever.
The Sleep command will tell PowerShell to sit and pick its nose for some period of time. But the Sleep command doesn't have a "wait until a certain time" switch. You have to specify exactly how many seconds (or milliseconds) you want it to just sit and pick its nose. So you need to figure out how many seconds there are between now and 7PM.
I was going to use Get-Date command to get the current date-time and do the math from there. Along the way I was going to expound about the benefits of having the date-time in an object instead of a string or number. I was going to explore the exploration of properties and methods. To get there I was going to "discover" Get-Date and its use by using Get-Command and Get-Help. Because it would make sense to search first on the word "time" before the word "date" when looking for a time command, I was going to do that first, and only go to "date" when "time" failed to yield useful results.
And that is when the article went off the rails.
Pretending to look for a time command, I typed:
Get-Command *time*
And there at the top of the list was the command New-TimeSpan. I initially dismissed New-TimeSpan, as I knew it wouldn’t be helpful in this scenario. But that would not be immediately apparent to a reader, so I thought I needed to say something about it. So I took a closer look.
I ran:
Get-Help New-TimeSpan
which showed:
"SYNTAX
New-TimeSpan [[-Start]
New-TimeSpan [-Days
DESCRIPTION
The New-TimeSpan cmdlet creates a TimeSpan object that represents a time interval You can use a TimeSpan object to add or subtract time from DateTime objects."
You can ask it to explain the switches and give some examples if you run:
Get-Help New-TimeSpan –Detailed
It still looked to me that my old method would work better, but I started to get an inkling that there may be some built-in shortcuts that can help us out.
Using the first syntax, New-TimeSpan calculates the period of time between two dates and/or times and uses the result to create a [system.timespan] object. New-TimeSpan takes [system.datetime] objects as input.
Start of tangent
Get-Date produces a [datetime] object with the current date and time. If I type:
Get-Date
I get:
Saturday, June 15, 2013 2:52:08 PM
That date isn’t just a string or number representing the date. It’s an object, with lots of properties and methods. “Methods” are little built-in chunks of code that we can leverage in our scripts.
We can see a list of all of the properties and methods of an object by piping it to a Get-Member command.
Get-Date | Get-Member
yields a long list you can explore on your own. But two useful ones four out testing here are AddDays() and AddHours().
Get-Date yields Saturday, June 15, 2013 2:53:12 PM
(Get-Date).AddDays(4) yields Wednesday, June 19, 2013 2:53:14 PM
(Get-Date).AddHours(6) yields Saturday, June 15, 2013 6:53:15 PM
We can use that to experiment with New-TimeSpan.
End of tangent
New-TimeSpan (Get-Date) (Get-Date)
yields 0 days. (Sometimes it yields the couple milliseconds that might elapse between the execution of the two Get-Date commands.)
We put the (Get-Date)’s in parentheses, because we are nesting commands. We have to tell PowerShell where the nested commands start and stop, so that it can complete those inner commands and use the results in the outer command.
New-TimeSpan (Get-Date) (Get-Date).AddDays(4).AddHours(6)
yields 4 days, 6 hours.
That doesn’t seem to help. We are just getting out of it what we are putting into it.
But here is where the shortcuts start to come in.
PowerShell is really, really good at dynamically and automatically converting variables of one type into a different type when needed. So if you put in a [string] where PowerShell needs a [datetime], PowerShell will do its best to convert it on the fly.
New-TimeSpan “6/14/2013” “6/18/2013”
yields 4 days.
New-TimeSpan “10:52 AM” “2:14 PM”
yields 3 hours, 22 minutes.
Software engineers hate this sort of thing, because it wastes precious CPU time to do the conversion. And for their purposes, they have a point. But we’re scripters, and scripters love this sort of thing. Because we don’t care if the computer spends an extra fraction of a second on a script, and it enables us to write code that is natural and intuitive and easy to read and understand.
The next question is, were the PowerShell architects smart enough and kind enough to allow further shortcuts that work in ways that are intuitive to me and useful to me.
What happens when we mix and match? If I give it a [datetime] and a string with just a time but no date:
New-TimeSpan (Get-Date) “7 PM”
yields 4 hours, 23 minutes, 13 seconds.
Perfect! When I give it just a time, it assumes I mean today.
Can I take it a step further? What happens if I leave out the start time altogether? I can’t just run the above command without the (Get-Date). If we leave off the parameter names, PowerShell will assume that the first parameter is a start time, not an end time. So I'll put the -End parameter name back in.
Let's try:
New-TimeSpan -End "7 PM"
It produces 4 hours, 22 minutes, 27 seconds. Awesome! It worked! When we leave off the start time, it assumes a start time of right now.
Now if you have been trying any of these on your own, instead of just kicking back and letting me do all of the work, you have seen that New-TimeSpan doesn't just produce the simple output I've been writing. [Timespan] is an object with lots of properties, which are displayed when you ask PowerShell to output a [timespan] object to a command line, which is what we've been doing. (Most objects don't show you all of their properties by default. To see a list of all of the properties of an object, pipe it to | Format-List * )
New-TimeSpan -End "7 PM"
actually results in:
Ticks : 387193663125
Days : 0
Hours : 10
Milliseconds : 366
Minutes : 45
Seconds : 19
TotalDays : 0.448140813802
TotalHours : 10.75537953125833
TotalMilliseconds : 38719366.3125
TotalMinutes : 645.322771875
TotalSeconds : 38719.3663125
Look at that last property. That's the number of seconds between now and 7PM. That's exactly what we needed for our Sleep command.
We reference a single property of an object with the syntax Object.PropertyName
I'll do it in two steps first, as it's easier to see that way. Let's assign our [timespan] to a variable.
$x = New-TimeSpan -End "7 PM"
Then we can reference the properties:
$x.TotalDays
yields 0.5478965
$x.TotalSeconds
yields 1476526.65248652
If we are not going to reuse the [timespan] we calculated, we don't need to store it in a variable. We can put parentheses around the New-TimeSpan command to tell Powershell where the nested command starts and ends, and then apply the .TotalSeconds to the parentheses thusly:
(New-TimeSpan -End "7 PM").TotalSeconds
Now we can complete our Sleep command:
Sleep -Seconds (New-TimeSpan -End "7 PM").TotalSeconds
Great Post Tim!
ReplyDeleteI liked how you went around and spiked my interest on reading it till end ;)
Thanks for sharing it
Perfect for what I wanted! Many thanks, well written.
ReplyDeleteNice post thanks! I was looking to do all kind of fancy sleep checks, this is golden :)
ReplyDeleteSleep can accept from pipe.
ReplyDeleteNew-TimeSpan -End "7 PM" | Sleep
Perfect, just what I needed thanks!
ReplyDeleteVery cool! I was doing it like this:
ReplyDeleteSleep -Seconds ((Get-Date 7pm) - (Get-Date)).Seconds
Nice post clear and interesting. Modified it to the following command to be used in a while loop:
ReplyDelete(Get-Date).AddSeconds((New-TimeSpan -End "4 PM").TotalSeconds)
Your website is really cool and this is a great inspiring article.
ReplyDeletesleep study
Great article. I've immediately put this to use. Thank you!!!!
ReplyDeleteI totally dig the way you wrote this... have learnt so much from it. Powershell++ ;)
ReplyDeleteHello,
ReplyDeleteTerrific article!
I’m launching a Powershell that starts at a variable time (11:3X PM) but must execute a program at 12:00:00 AM sharp. Therefore, I thought the timespan method would be ideal. However, when I try to span the meridiem (PM to AM) I get a “ge of 0” exception (see below) apparently because the value returned for Seconds is negative.
Can anyone please recommend an appropriate method that can span the meridiem? I have also tried similar tricks with “Get-Date” which yielded a similar exception.
Thanks very much in advance!
PS C:\> Get-Date
Thursday, October 04, 2018 11:55:44 PM
PS C:\> Sleep -Seconds (New-TimeSpan -End "12:00 AM").TotalSeconds
Start-Sleep : Cannot validate argument on parameter 'Seconds'. The -86146 argument is less than the minimum allowed ran
ge of 0. Supply an argument that is greater than 0 and then try the command again.
At line:1 char:15
+ Sleep -Seconds <<<< (New-TimeSpan -End "12:00 AM").TotalSeconds
+ CategoryInfo : InvalidData: (:) [Start-Sleep], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,Microsoft.PowerShell.Commands.StartSleepCommand
PS C:\>
For the special case of 12:00 AM you can do this:
DeleteStart-Sleep -Seconds ( 24*60*60 - (Get-Date).TimeOfDay.TotalSeconds )
For something that works with any time of day which may or may not be tomorrow, this works. it's ugly, but it works.
Start-Sleep -Seconds ( New-TimeSpan -End ( $X = [datetime]'12:00 AM' ).AddDays( $X -lt (Get-Date) ) ).TotalSeconds
Sleep -Seconds ((New-TimeSpan -End "11:59 PM").TotalSeconds + 60)
DeleteHello Tim,
ReplyDeleteThe ugly method worked great and didn’t create a conflict for jobs with the same meridiem.
Thanks!