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

Delete all blank lines from a text file using PowerShell

Get-Content $FilePath ) | Where { $_ } | Set-Content $FilePath

Or for those inclined towards brevity:

(GC $FP)|?{$_}|SC $FP

We generally use PowerShell for larger, more complex tasks.  But don’t you love elegant one-liners?
This one line will strip out all of the blank lines from a text file.

How it works

In short, we are reading all of the lines of the file, and then writing them back to the file, skipping any lines that are blank.

We presume variable $FilePath contains the string with the full path to the file we are working with.

$FilePath = "D:\Test\LogFile.txt"

We put the parenthesis around the Get-Content statement, because if we don’t , the end of the pipeline will be trying to write to the same file that the beginning of the pipeline is still trying to read from, and throw an error.  The parentheses force it to finish loading the contents into an object before sending them down the pipeline.  (If we were writing to a different file than we were reading from, we could speed up the command by eliminating the parenthesis, thus allowing us to read from the one and write to the other simultaneously.)

The individual items traveling through the pipeline will be strings representing each line of the contents.  The Where { $_ } clause effectively converts each string to a boolean value for testing.  Every non-empty string evaluates as $True and is passed on to be rewritten back to the file.  Every empty string evaluates as $False, and is not passed on.

Our original version of the code will only strip out completely empty lines:

( Get-Content $FilePath ) | Where { $_ } | Set-Content $FilePath

But if our "blank" lines are filled with spaces and tabs, we need to strip those characters out as well. 

In PowerShell, every variable is an object, and objects come with "methods", which are built-in pieces of code that we can leverage.  If we pipe a string to Get-Member, it gives us a list of methods for strings.

$X = ""
$X.GetType()

Or just…

("").GetType()

One of the string methods is Trim, which can be used to strip undesired characters from the start and end of a string.

$X.Trim(" ")
Will delete any spaces at the start or end of string variable $X.

$X.Trim("`t")
Will delete any tabs.

$X.Trim(" `t")
Will delete all spaces and all tabs.

$X.Trim(" `t")
, when converted to a Boolean value, will evaluate to $False if $X has nothing but spaces and or tabs in it.  (Or nothing in it.)

So, to strip out all blank lines, including those with spaces and tabs, from a text file, we can use:

( Get-Content $FilePath ) | Where { $_.Trim(" `t" } | Set-Content $FilePath

Or

(GC $FP)|?{$_.Trim(" `t")}|SC $FP

(If you need to get fancier, one or more Select-String statements can be used instead of our Where-Object command, but getting fancy with the "regular expressions" that Select-String uses can get quite complicated and confusing.  When all we need is simple, let’s keep it simple.)