Tuesday 23 December 2014

Splitting Strings in PowerShell

I had a need to split some strings the other day, which turned out to be a little more complicated that I was expecting.  Take this string:

$string = "this is a very nice string"

Splitting this on, say, the letter 'v' is as easy as I thought it would be.

$string.Split("v")

This is a

ery nice string

Nice.  I had to split on a string though, such as "very".

$string.Split("very")

This is a



 nic
 st

ing

Uhm, what happened there?  Well, calling the Split() method as we have done actually passes an array of characters rather than a string.  This is an important distinction as we're now splitting on 'v' or 'e' or 'r' or 'y' instead of "very".  There is a simple alternative in the -split paramter.

$string -split "very"

This is a

 nice string

That's the result I was hoping for!  Job done.

Not really.  .Split() was nagging away at me.  There must be a way to make that work?  Where do I find out if there is any way to call Split() with a string?  From MSDN.  MSDN is a great resource.  It's not the first place I would go to when I need a tutorial or I'm picking up a topic for the first time but if you have a good idea of what you're looking for and need a detailed reference guide, it's an excellent place.

Searching MSDN for "String.Split Method" gets me here.  This page lists all the overloads for the method.


This shows that there is one overload method that has a single argument and that argument must be a character array.  This explains the behaviour we saw above.  However, looking down that list, there are two overload methods that both support string arrays as separators!  Exactly what I was looking for!  

Split(String[], StringSplitOptions)
Split(String[], Int32, StringSplitOptions)

Both these overloads require the StringSplitOptions argument.  This is defined here but basically all this argument defines is whether or not we want blank entries returned in the array or left out.  The second overload also contains an Int32 argument which can be used to define the maximum number of substrings that are returned.  So if we only wanted the first substring returned after the split, we could use a "1" there.

StringSplitOptions are defined as follows:

[System.StringSplitOptions]::None
[System.StringSplitOptions]::RemoveEmptyEntries

The first option returns array elements that include blank strings, the second one removes them.  It doesn't matter which we use, but we need to use one to match the overloads that allow us to pass a string.  Speaking of which, we can't just use an argument of "very" either.  You will see that there are also overloads for the character array arguments that include the StringSplitOptions argument as well.  This means that "very" will still be treated as a character array and we'll be back to square one.  We need to define it as a string.

@("very")

will do nicely.  In the end, this means we can do this:

$string.Split(@("very"), [System.StringSplitOptions]::None)

This is a

 nice string

Yay!  Now, is that easier that using -split?  Well, no, it's not.  But I scratched an itch.



Wednesday 10 December 2014

Monitoring Shadow Copies with PowerShell




Shadow Copies are a wonderful thing.  Yup, they're not a replacement for backups but they are not meant to be.  What they are is a very quick, very simple way to restore data.

For peace of mind, I like to have a quick glance over my servers to make sure Shadow Copies are turned on for the drives I need and are creating copies on schedule.  That's the sort of regular, repetitive task that PowerShell is great for.

I'm a fan of using PowerShell with Get-WMIObject to get data back from WMI and there's a WMI class for managing Shadow Copies called, well, Win32_ShadowCopy.  Get-WMIObject Win32_ShadowCopy $servername will get you a list of all current shadow copies on that server for all drives.  If you run that against a server with no shadow copies, all you're going to get back is an "InvalidOperation" error.

There are two properties of Win32_ShadowCopy that I'm really interested in, InstallDate and VolumeName.  InstallDate holds the date the shadow copy was created and VolumeName holds the ID of the device (drive) that the shadow copy was created on.  Both these fields need a little manipulating to be useful though.

A typical InstallDate is going to look something like this.

20141201094535.549577+000.  

I'm only interested in the part before the period which is the date and time in the format yyyymmddhhmmss as a long string so we need to break this up a bit.  Something like this.

$date = [datetime]::ParseExact($copy.InstallDate.Split(".")[0], "yyyyMMddHHmmss", $null)

That looks a little complicated but it's not really.  What we're doing is creating a new variable, using [datetime] to force the type and ParseExact to turn the string into something that's usable as a datetime.  The bit after ParseExact takes the string, splits it at the period - which gives a two element array, one before and one after the period - uses [0] to select the first element of that new array and uses the string yyyyMMddHHmmss to define which parts of that string to use as each element of the datetime.  Case is important here, for example, hh signifies hours in a 12-hour clock but HH signifies them in a 24-hour clock.  If you use the wrong case, you're going to get the wrong time.  There's great info on that here.  What we're left with is a variable called $date which holds the date and time the snapshot was created.  Perfect.  How do we find the drive though?  We use the VolumeName attribute, which looks like this.

\\?\Volume{aja08123-6c44-12e2-7693-006021310173}\

That actually looks worse than what we started with for the InstallDate!  This is an identifier for the drive but it doesn't really turn us anything.  I'd much rather see a drive letter here.  Fortunately, we can turn this ID into a drive letter using another wmi class, Win32_Volume.  Get-WMIObject Win32_Volume -ComputerName $server returns a list of all volumes present on $sever.  There's a lot of info in this class but only two properties I'm interested in here, DeviceID and Name.  DeviceID holds the same value that we get back from the VolumeName attribute in Win32_ShadowCopy and Name holds the relevant drive letter.  Perfect.  Actually, there are two other attributes you can use to get the driveletter, both DriveLetter and Caption.  Any of these three will work although the actual DriveLetter parameter does not include the trailing backslash.  Take your pick.  What we can do though, is create a hashtable then go through each result returned from Win32_Volume and add both the DeviceID and Name to the hashtable.

$volumes = @{}
$allvolumes = Get-WmiObject win32_volume -ComputerName $server -Property DeviceID, Name
foreach ($v in $allvolumes)
    {
        $volumes.add($v.DeviceID, $v.Name)
    }
$volumes

This is cool.  Now to return a drive letter, we only need to use the following.

$driveletters.Item($VolumeName)

How easy is that?  We give it the VolumeName we get back from Win32_ShadowCopy and it gives us the drive letter.  So now we need to wrap all this in a loop to iterate through all the shadow copies and find the date/time they were created and which drive they were created on.  That'll look something like this.

There are *lots* of improvements to be made here.  Error checking is non-existent, the output format could be tidied up and this really should be scheduled and emailed but it's a good place to start.

Friday 5 December 2014

PowerShell and Notepad++ - Adding custom keyword highlighting

I've been playing around with using Notepad++ as my PowerShell script editor of choice.  I like ISE, I just fancied a change.  Sure, ISE has some really nice intellisense and syntax highlighting features but Notepad++ just feels quicker.

Notepad++ includes native support for PowerShell syntax highlighting (yay!) but this didn't include all the cmdlets I have available on my system.  This isn't surprising, there's a huge amount available through various modules, but it's a little bit annoying to have some highlighted and others not.

There is support for adding custom keywords through the style configurator (see here for an example) but I didn't like this.  I had far too many to add for a start.  And it messed up my stylers.xml file.  I'm a bit anal about this but when I added a keyword through the style configurator my stylers.xml changed from:

<WordsStyle name="CMDLET" styleID="9" fgColor="804000" bgColor="293134" fontName="" fontStyle="0" fontSize="" keywordClass="instre2">

to:

<WordsStyle name="CMDLET" styleID="9" fgColor="804000" bgColor="293134" fontName="" fontStyle="0" fontSize="" keywordClass="instre2">new-keyword</WordsStyle>

Yeah, that annoyed me.  It just looked kinda messy.  Especially when there's a really nice list of keywords already in langs.xml.

<Keywords name="instre2">add-content add-history add-member add-pssnapin clear-content clear-item clear-itemproperty...etc...</Keywords>

I really don't think editing langs.xml directly is supported.  I think you run the risk of losing your changes on the next version of Notepad++.  It's much prettier though.  So all I need to do is add the missing keywords into that xml tag and job's a good 'un.  So where do I get them from?  From PowerShell.

Using the Get-Command cmdlet after loading all the modules I use will show me all cmdlets (yay!).  With a bit of tinkering this can be formatted to suit the xml format of the tags.  That being, space between each tag and everything in lower case.  Lower case seems to be important, it wouldn't work without that.  That gave me this:

[string]$commands = (get-command).name; foreach ($c in $commands) {$c.ToLower() + " " >> commands.txt}

Long story short, this pulls all the cmdlets from my PowerShell session and leaves them in commands.txt in a format that's ready to cut and paste into langs.xml.  There's probably a nicer way to do this but, hey, I'm just learning PowerShell.

Copy and paste the contents of commands.txt into langs.xml in the <Keywords name="instre2">  tag and you get syntax highlighting for them all.  Very nice!

One other thing to point out, you will find some extra entries in your keywords you weren't expecting, such as all your drive letters.  That's because they are technically a command in PowerShell.  You can either tidy these up manually, regex them out or improve on the initial command above so they don't appear in the first place.  I haven't quite gotten around to that yet.