Tuesday, August 13, 2013

Sleep until a certain time with PowerShell

Outsmarted by PowerShell

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] ] [[-End] ] []
New-TimeSpan [-Days ] [-Hours ] [-Minutes ] [-Seconds ] []

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

6 comments:

  1. Great Post Tim!
    I liked how you went around and spiked my interest on reading it till end ;)

    Thanks for sharing it

    ReplyDelete
  2. Perfect for what I wanted! Many thanks, well written.

    ReplyDelete
  3. Nice post thanks! I was looking to do all kind of fancy sleep checks, this is golden :)

    ReplyDelete
  4. Sleep can accept from pipe.

    New-TimeSpan -End "7 PM" | Sleep

    ReplyDelete
  5. Perfect, just what I needed thanks!

    ReplyDelete
  6. Very cool! I was doing it like this:
    Sleep -Seconds ((Get-Date 7pm) - (Get-Date)).Seconds

    ReplyDelete