Measure the Comment-Based Help Synopsis

There was a recent tweet — I believe it was a tweet, anyway — that indicated that a comment-based help synopsis, should only be as long as what’s allowable for a tweet. That’s 140 characters. With that in mind and a few minutes, I wrote this little function to check the character count in a string.

Function Measure-CharacterCount {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true)]
        [string]$String,

        [int]$CharacterCount = 140
    )
    $StringCount = $String.ToCharArray().Count

    If ($StringCount -le $CharacterCount) {
        Write-Verbose -Message "The string is equal to or less than $CharacterCount characters ($StringCount)." -Verbose
    } Else {
        Write-Warning -Message "The string is longer than $CharacterCount characters ($StringCount)."
    }
}

In the example below, I’ve included two commands: one where the character count is less than 140 and one where it’s greater. On that note, let me add a third where the character count equals the default 140 character, and then 141, just to make sure this thing works…

PS > Measure-CharacterCount -String 'Today is awesome, maybe.'
VERBOSE: The string is equal to or less than 140 characters (24).
PS >
PS > Measure-CharacterCount -String 'Today is awesome, maybe. Today is awesome, maybe. Today is awesome, maybe. Today is awesome, maybe. Today is awesome, maybe. Today is awesome, maybe.'
WARNING: The string is longer than 140 characters (149).
PS > 
PS > Measure-CharacterCount -String 'Today is awesome, maybe. Today is awesome, maybe. Today is awesome, maybe. Today is awesome, maybe. Today is awesome, maybe. Today is awesom'
VERBOSE: The string is equal to or less than 140 characters (140).
PS >
PS > Measure-CharacterCount -String 'Today is awesome, maybe. Today is awesome, maybe. Today is awesome, maybe. Today is awesome, maybe. Today is awesome, maybe. Today is awesome'
WARNING: The string is longer than 140 characters (141).

Post 200!

I’ve stopped all technical PowerShell babbling for a moment to point out the newest tommymaynard.com accomplishment. In less than three years’ time, I’ve written 200 published posts. That’s over six posts per month for a few months under three consecutive years. About PowerShell. Written by me. With the hopes to help people learn about PowerShell, or find a solution to a problem.

Back soon with something new. Enjoy the weekend.

PSMonday #48: March 27, 2017

Topic: The (Online) End of PowerShell Monday

For those following along online, as opposed to those at my workplace, this is the end of the road for PowerShell Monday. The following five weeks of PSMondays are work only, as they describe an advanced function template I’ve written, for those writing PowerShell tools in the office. The reason I’ve decided to end here, is that the posts may be a bit more work specific, and that I’ve actually already offered up this function for public consumption.

At work, we always wanted a standard logging function. Instead of writing that, I incorporated the logging ability into a function template that’s usable for any PowerShell tool we write. This means, that there’s no separate logging function for our functions. It’s built in, if you need it. Before I decided this is something we really should use at work, I actually shared it here first. You can already learn about it, see it for yourself, download it, and use it all you want. The way I accomplished this logging, is by using Write-Verbose to write to the host program, and to a log file, too.

Thanks to those that read the PSMonday series, or any of my posts, really.
http://tommymaynard.com/function-logging-via-write-verbose-2016/

Get the Verbs

So, earlier tonight, my wife, daughter, and I were on the couch. My wife was cleaning out her purse as my daughter helped her organize her change. Me, I was briefly on my phone and looking for a quick list of approved PowerShell verbs. As you likely know, if we create our own cmdlets and functions, we should use an approved verb for the verb, dash, singular noun naming convention: Get-Process, Set-ADUser, Checkpoint-Computer… okay, it’s not always a verb, but you know what I mean.

I couldn’t believe it; I couldn’t just find a simple list of all the verbs, close to one another, and without any explanations. You know the list, the one you basically get when you run Get-Verb. I suppose I should mention that all I had was my phone. There wasn’t a computer close by, and that’s, why I’m putting up a list of the approved verbs (as of PowerShell 5.1 on Windows 8.1), on my website.

Here’s the standard, Get-Verb output that includes the Name and Group. After that, I’ll include the same list, a few more ways. Maybe, just maybe it’ll be here when you, or I, need it next.

PS > Get-Verb

Verb        Group
----        -----
Add         Common
Clear       Common
Close       Common
Copy        Common
Enter       Common
Exit        Common
Find        Common
Format      Common
Get         Common
Hide        Common
Join        Common
Lock        Common
Move        Common
New         Common
Open        Common
Optimize    Common
Pop         Common
Push        Common
Redo        Common
Remove      Common
Rename      Common
Reset       Common
Resize      Common
Search      Common
Select      Common
Set         Common
Show        Common
Skip        Common
Split       Common
Step        Common
Switch      Common
Undo        Common
Unlock      Common
Watch       Common
Backup      Data
Checkpoint  Data
Compare     Data
Compress    Data
Convert     Data
ConvertFrom Data
ConvertTo   Data
Dismount    Data
Edit        Data
Expand      Data
Export      Data
Group       Data
Import      Data
Initialize  Data
Limit       Data
Merge       Data
Mount       Data
Out         Data
Publish     Data
Restore     Data
Save        Data
Sync        Data
Unpublish   Data
Update      Data
Approve     Lifecycle
Assert      Lifecycle
Complete    Lifecycle
Confirm     Lifecycle
Deny        Lifecycle
Disable     Lifecycle
Enable      Lifecycle
Install     Lifecycle
Invoke      Lifecycle
Register    Lifecycle
Request     Lifecycle
Restart     Lifecycle
Resume      Lifecycle
Start       Lifecycle
Stop        Lifecycle
Submit      Lifecycle
Suspend     Lifecycle
Uninstall   Lifecycle
Unregister  Lifecycle
Wait        Lifecycle
Debug       Diagnostic
Measure     Diagnostic
Ping        Diagnostic
Repair      Diagnostic
Resolve     Diagnostic
Test        Diagnostic
Trace       Diagnostic
Connect     Communications
Disconnect  Communications
Read        Communications
Receive     Communications
Send        Communications
Write       Communications
Block       Security
Grant       Security
Protect     Security
Revoke      Security
Unblock     Security
Unprotect   Security
Use         Other

As you may have noticed in the previous results, the default output is sorted by the Group. Here comes a second list; however, this list will be sorted by the Verb. I can already see how this may be helpful. In fact, this is the list I was probably after.

PS > Get-Verb | Sort-Object -Property Verb

Verb        Group
----        -----
Add         Common
Approve     Lifecycle
Assert      Lifecycle
Backup      Data
Block       Security
Checkpoint  Data
Clear       Common
Close       Common
Compare     Data
Complete    Lifecycle
Compress    Data
Confirm     Lifecycle
Connect     Communications
Convert     Data
ConvertFrom Data
ConvertTo   Data
Copy        Common
Debug       Diagnostic
Deny        Lifecycle
Disable     Lifecycle
Disconnect  Communications
Dismount    Data
Edit        Data
Enable      Lifecycle
Enter       Common
Exit        Common
Expand      Data
Export      Data
Find        Common
Format      Common
Get         Common
Grant       Security
Group       Data
Hide        Common
Import      Data
Initialize  Data
Install     Lifecycle
Invoke      Lifecycle
Join        Common
Limit       Data
Lock        Common
Measure     Diagnostic
Merge       Data
Mount       Data
Move        Common
New         Common
Open        Common
Optimize    Common
Out         Data
Ping        Diagnostic
Pop         Common
Protect     Security
Publish     Data
Push        Common
Read        Communications
Receive     Communications
Redo        Common
Register    Lifecycle
Remove      Common
Rename      Common
Repair      Diagnostic
Request     Lifecycle
Reset       Common
Resize      Common
Resolve     Diagnostic
Restart     Lifecycle
Restore     Data
Resume      Lifecycle
Revoke      Security
Save        Data
Search      Common
Select      Common
Send        Communications
Set         Common
Show        Common
Skip        Common
Split       Common
Start       Lifecycle
Step        Common
Stop        Lifecycle
Submit      Lifecycle
Suspend     Lifecycle
Switch      Common
Sync        Data
Test        Diagnostic
Trace       Diagnostic
Unblock     Security
Undo        Common
Uninstall   Lifecycle
Unlock      Common
Unprotect   Security
Unpublish   Data
Unregister  Lifecycle
Update      Data
Use         Other
Wait        Lifecycle
Watch       Common
Write       Communications

In this example, all I want to return is the verbs. Nine times out of 10, the group doesn’t make a difference to me.

PS > (Get-Verb | Sort-Object -Property Verb).Verb

Add
Approve
Assert
Backup
Block
Checkpoint
Clear
Close
Compare
Complete
Compress
Confirm
Connect
Convert
ConvertFrom
ConvertTo
Copy
Debug
Deny
Disable
Disconnect
Dismount
Edit
Enable
Enter
Exit
Expand
Export
Find
Format
Get
Grant
Group
Hide
Import
Initialize
Install
Invoke
Join
Limit
Lock
Measure
Merge
Mount
Move
New
Open
Optimize
Out
Ping
Pop
Protect
Publish
Push
Read
Receive
Redo
Register
Remove
Rename
Repair
Request
Reset
Resize
Resolve
Restart
Restore
Resume
Revoke
Save
Search
Select
Send
Set
Show
Skip
Split
Start
Step
Stop
Submit
Suspend
Switch
Sync
Test
Trace
Unblock
Undo
Uninstall
Unlock
Unprotect
Unpublish
Unregister
Update
Use
Wait
Watch
Write

This next example uses Format-Wide so that my results are in columns. Notice how the sorting goes across the rows, as opposed to down the columns. There’s should probably be a built-in way to handle that behavior. Yuck.

PS > Get-Verb | Sort-Object -Property Verb | Format-Wide -Column 4

Add                           Approve                       Assert                        Backup
Block                         Checkpoint                    Clear                         Close
Compare                       Complete                      Compress                      Confirm
Connect                       Convert                       ConvertFrom                   ConvertTo
Copy                          Debug                         Deny                          Disable
Disconnect                    Dismount                      Edit                          Enable
Enter                         Exit                          Expand                        Export
Find                          Format                        Get                           Grant
Group                         Hide                          Import                        Initialize
Install                       Invoke                        Join                          Limit
Lock                          Measure                       Merge                         Mount
Move                          New                           Open                          Optimize
Out                           Ping                          Pop                           Protect
Publish                       Push                          Read                          Receive
Redo                          Register                      Remove                        Rename
Repair                        Request                       Reset                         Resize
Resolve                       Restart                       Restore                       Resume
Revoke                        Save                          Search                        Select
Send                          Set                           Show                          Skip
Split                         Start                         Step                          Stop
Submit                        Suspend                       Switch                        Sync
Test                          Trace                         Unblock                       Undo
Uninstall                     Unlock                        Unprotect                     Unpublish
Unregister                    Update                        Use                           Wait
Watch                         Write

In the next example, I’ll try the -join operator.

PS > (Get-Verb | Sort-Object -Property Verb).Verb -join ', '
Add, Approve, Assert, Backup, Block, Checkpoint, Clear, Close, Compare, Complete, Compress, Confirm, Connect, Convert, C
onvertFrom, ConvertTo, Copy, Debug, Deny, Disable, Disconnect, Dismount, Edit, Enable, Enter, Exit, Expand, Export, Find
, Format, Get, Grant, Group, Hide, Import, Initialize, Install, Invoke, Join, Limit, Lock, Measure, Merge, Mount, Move,
New, Open, Optimize, Out, Ping, Pop, Protect, Publish, Push, Read, Receive, Redo, Register, Remove, Rename, Repair, Requ
est, Reset, Resize, Resolve, Restart, Restore, Resume, Revoke, Save, Search, Select, Send, Set, Show, Skip, Split, Start
, Step, Stop, Submit, Suspend, Switch, Sync, Test, Trace, Unblock, Undo, Uninstall, Unlock, Unprotect, Unpublish, Unregi
ster, Update, Use, Wait, Watch, Write

Also yuck. So yeah, I thought that might turn out better. Well, I’ve got it now — a place to find all the current verbs as of this writing, if I’m ever sitting on the couch, watching two of my favorite people, and unable to decide what verb to use for a new PowerShell tool.

PSMonday #47: March 20, 2017

Topic: Reusable Code V

Notice: This post is a part of the PowerShell Monday series — a group of quick and easy to read mini lessons that briefly cover beginning and intermediate PowerShell topics. As a PowerShell enthusiast, this seemed like a beneficial way to ensure those around me at work were consistently learning new things about Windows PowerShell. At some point, I decided I would share these posts here, as well. Here’s the PowerShell Monday Table of Contents.

I know, I know, I said this would be a four part series, but we’re adding a fifth installment; it’s going to be worth it.

Now that we have a function we want to use whenever it makes sense, we need a place to put it. While we could dot source our Process.ps1 file every time we want to add the function to our scope (like we did last week), there’s two better options. The first option is the profile script. This is a .ps1 file that runs every time you open a new PowerShell ConsoleHost. There’s actually one for the ISE, too.

The below example shows how to determine where the profile script should be located for the current host program (the ConsoleHost).

$PROFILE

C:\Users\tommymaynard\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1

Now, just because we know the path and file, doesn’t mean the file actually exists. The next thing we should do is test the path to determine if the file exists, or not. Test-Path is a helpful cmdlet, in that it can determine if files and folders exist.

Test-Path -Path $PROFILE

False

If you received False when you run the above command, you don’t yet have a profile script. If you do have one, it’ll indicate True. For those with False, let’s quickly create a profile script and then open it.

New-Item -Path $PROFILE -ItemType File -Force

Once this command is run, it’ll indicate that it’s created the profile script file. It’s a simple text file, so it can be opened and edited a number of different ways. As of now, I tend to modify my profile script in the PowerShell ISE. To edit yours in the ISE, use the first below example. To use Notepad, use the second one. For those already using Visual Studio Code, you can open it there as well.

ise $PROFILE

notepad $PROFILE

With your profile script opened, you can add the function we finished with last Monday. It’s included below.

Function Get-TMService {
    Param (
        [Parameter(Mandatory=$true)]
        [string]$Service
    )
 
    try {
        Get-Process -Name $Service -ErrorAction Stop |
            Select-Object -Property @{N='Process Name';E={$_.Name}},
                Description,
                Company,
                @{N='Shared Memory';E={"$([Math]::Round($_.WorkingSet / 1MB)) MB"}},
                @{N='Private Memory Size';
                    E={"$([Math]::Round($_.PrivateMemorySize / 1MB)) MB"}}
    } catch {
        Write-Warning -Message "Cannot locate the $Service process."
    }
}

Now, if you’re following along, enter the below command.

Get-TMService -Service powershell_ise

This command should fail, whether or not, the PowerShell ISE is running. It’s because you added the function to your profile script after the profile script was last run. Close the PowerShell ConsoleHost and then open a new one. Doing this will ensure the recently added function is available in your PowerShell session. At this point, the above Get-TMService command should run successfully.

The second option is to use a PowerShell module. Creating a module can get much more complex, as can most things, but there’s just a few steps to make it work at its simplest form. A module is typically created to hold several related functions; however, it can only hold one function, if that’s all you have for now.

First, copy the above function into a text file, giving it an appropriate name, and saving it as a .psm1 file. For instance, call the new file MyFunctions.psm1 and paste the function code inside of it. Now, create a folder with the same name as the module file (without the file extension). This means your folder would be called MyFunctions. The name of the file and the folder must be the same. Now, move the file into the folder.

Next, we need to relocate this folder to a location in the filesystem where PowerShell will look for modules automatically. You can see those locations using the below example.

$env:PSModulePath -split ';'

C:\Users\tommymaynard\Documents\WindowsPowerShell\Modules
C:\Program Files\WindowsPowerShell\Modules
C:\Windows\system32\WindowsPowerShell\v1.0\Modules\

You’re welcome to use “C:\Program Files\WindowsPowerShell\Modules\” if you want all users on the computer to use the module. If you’re happy with just you being able to use your module, then place it in your profile folder. The final above path, in System32, shouldn’t be bothered if you can help it.

With this completed, the next time you open the PowerShell ConsoleHost you’re module, and that contained function, will be available. If you write some more functions, you can always add them to this same module file, just like you could your profile script.

PSMonday #46: March 13, 2017

Topic: Reusable Code IV

Notice: This post is a part of the PowerShell Monday series — a group of quick and easy to read mini lessons that briefly cover beginning and intermediate PowerShell topics. As a PowerShell enthusiast, this seemed like a beneficial way to ensure those around me at work were consistently learning new things about Windows PowerShell. At some point, I decided I would share these posts here, as well. Here’s the PowerShell Monday Table of Contents.

The below example is the same one we ended on last Monday. What I need you to imagine is that this code now exists in a file called Process.ps1, on the desktop of your own computer. You can even put it there if you want to follow along.

$Service = Read-Host -Prompt 'Enter a Process Name'

try {
    Get-Process -Name $Service -ErrorAction Stop |
        Select-Object -Property @{N='Process Name';E={$_.Name}},
            Description,
            Company,
           @{N='Shared Memory';E={"$([Math]::Round($_.WorkingSet / 1MB)) MB"}},
           @{N='Private Memory Size';
               E={"$([Math]::Round($_.PrivateMemorySize / 1MB)) MB"}}
} catch {
    Write-Warning -Message "Cannot locate the $Service process."
}

In the next example, we’ll begin by changing directories to the desktop. Once there, we’ll run our Process.ps1 file that contains the code we’ve been constructing over the last few weeks. As you’re likely aware, changing directories — the first below command — isn’t necessary, had we simply supplied the full path to the Process.ps1 file in the second command.

PS > Set-Location -Path C:\Users\tommymaynard\Desktop

PS > .\Process.ps1
Enter a Process Name: powershell_ise

Process Name : powershell_ise
Description : Windows PowerShell ISE
Company : Microsoft Corporation
Shared Memory : 181 MB
Private Memory Size : 159 MB

PS > # FYI: Notepad is not currently running.
PS > .\Process.ps1
Enter a Process Name: notepad
WARNING: Cannot locate the notepad process.

After instructing the .ps1 to execute, it immediately prompts us to enter a process name just as we’ve seen in the past. If we enter something that’s running, it returns our modified information according to the command we’ve written. If it’s not running, it displays the Write-Warning message as opposed to throwing the red, and sometimes intimidating, error message we saw last week. What an easy way to return only what you want, and without the need to rewrite the code each time.

Here’s the thing: People love the Read-Host cmdlet, but there’s a better way. Let’s use a Param block instead. This next example does just that. As you move into writing functions, you’ll quickly begin to understand why parameters are better. So you can continue to picture this, we’re saving the below modification over what we had in the Process.ps1 file on the desktop. Watch how we run it now!

Param (
    [string]$Service
)
 
try {
    Get-Process -Name $Service -ErrorAction Stop |
        Select-Object -Property @{N='Process Name';E={$_.Name}},
            Description,
            Company,
            @{N='Shared Memory';E={"$([Math]::Round($_.WorkingSet / 1MB)) MB"}},
            @{N='Private Memory Size';
                E={"$([Math]::Round($_.PrivateMemorySize / 1MB)) MB"}}
} catch {
    Write-Warning -Message "Cannot locate the $Service process."
}
 
PS > .\Process.ps1 -Service notepad
WARNING: Cannot locate the notepad process.
 
PS > .\Process.ps1 -Service powershell_ise
 
 
Process Name        : powershell_ise
Description         : Windows PowerShell ISE
Company             : Microsoft Corporation
Shared Memory       : 178 MB
Private Memory Size : 147 MB

Now, instead of the code using Read-Host to prompt us for input, we have a -Service parameter we can use. When the code was invoked, it took the parameter value we supplied and used it to set the $Service variable. There’s one problem here, however. If we don’t enter the Service parameter, the function will still try and check for a service that wasn’t even included. Let’s make the Service parameter mandatory; we can do that with one quick addition to our code.

Param (
    [Parameter(Mandatory=$true)]
    [string]$Service
)
 
try {
    Get-Process -Name $Service -ErrorAction Stop |
        Select-Object -Property @{N='Process Name';E={$_.Name}},
            Description,
            Company,
            @{N='Shared Memory';E={"$([Math]::Round($_.WorkingSet / 1MB)) MB"}},
            @{N='Private Memory Size';
                E={"$([Math]::Round($_.PrivateMemorySize / 1MB)) MB"}}
} catch {
    Write-Warning -Message "Cannot locate the $Service process."
}

Now, if we forget the Service parameter value, it’ll prompt us to enter it. I had to manually enter “powershell_ise” when it prompted me on the fourth line down, where it says “Service:”.

PS > .\Process.ps1
cmdlet Process.ps1 at command pipeline position 1
Supply values for the following parameters:
Service: powershell_ise
 
 
Process Name        : powershell_ise
Description         : Windows PowerShell ISE
Company             : Microsoft Corporation
Shared Memory       : 188 MB
Private Memory Size : 156 MB

The ultimate goal of someone that really wants to learn PowerShell well, is to lose the whole I-write-scripts mentality, and embrace an I-write-functions mentality. Let’s take our code and make it a function; there’s literally two changes.

We add a new first line that uses the Function keyword. Following that, we give our function a name and add an open, curly brace. Remember as you create functions, to use approved verbs (see Get-Verb for the approved list), a dash, and a singular noun, or singular nouns. You might also add a prefix to the noun, too, such as I’ve done by adding “TM.” This ensures my function won’t take precedence over the Microsoft written and included Get-Service cmdlet (that we actually use inside the Get-TMService function).

Function Get-TMService {
    Param (
        [Parameter(Mandatory=$true)]
        [string]$Service
    )
 
    try {
        Get-Process -Name $Service -ErrorAction Stop |
            Select-Object -Property @{N='Process Name';E={$_.Name}},
                Description,
                Company,
                @{N='Shared Memory';E={"$([Math]::Round($_.WorkingSet / 1MB)) MB"}},
                @{N='Private Memory Size';
                    E={"$([Math]::Round($_.PrivateMemorySize / 1MB)) MB"}}
    } catch {
        Write-Warning -Message "Cannot locate the $Service process."
    }
}

So it’s been said, we’ve again taken the above code and saved it over what we had in Process.ps1. Here’s the trick with using functions that are saved in .ps1 files. To add the Get-TMService function to our PowerShell session, we need to dot source the file. This will make the function available for use. The second below command has a dot and a space before the path and the file name (. .\Process.ps1). That dot is vital, so don’t overlook it. Let’s start by ensuring the function doesn’t yet exist.

PS > Get-Command -Name Get-TMService
Get-Command : The term 'Get-TMService' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again...
 
PS > . .\Process.ps1
 
PS > Get-Command -Name Get-TMService
 
CommandType     Name             Version    Source     
-----------     ----             -------    ------     
Function        Get-TMService
 
 
PS > Get-TMService -Service powershell_ise
 
 
Process Name        : powershell_ise
Description         : Windows PowerShell ISE
Company             : Microsoft Corporation
Shared Memory       : 198 MB
Private Memory Size : 174 MB

Had you read this and the previous three lessons, you’ve seen how we started with a simple command and turned it into a tool that we can use whenever it’s needed. If you give PowerShell some time now, you’re going to get your time back in the future. Until next week.

A Variable’s Current and Previous Value

How to Store a Variable’s Previous Value with the New Value

There was a recent post on the PowerShell Facebook group that said this: “Is there a function or option to check previous $Global:variable -ne Current $Global:variable?” The person that created the post wanted to know if there’s a way to recall the last value a variable stored, once it had already been assigned something new. Here’s my answer, and potential solution.

You have to think about a variable’s previous assignment, as if it never existed, even though you know that variable held something, or was assigned something, previously. Once its value is gone, it’s gone. Now, all that said, I came up with an option. The option, is to use a variable’s description property to store its previous value.

Let’s begin by checking the value of an uninitialized variable, and then assign it something.

PS > $x
PS > # No value.
PS > $x = 'first value'
PS > $x
first value

Now that our variable has a value, let’s take a look at all the variable’s properties using the Get-Variable cmdlet. Notice the Description property; we’re going to use this in a less than conventional way.

PS > Get-Variable -Name x | Select-Object -Property *

Name        : x
Description :
Value       : first value
Visibility  : Public
Module      :
ModuleName  :
Options     : None
Attributes  : {}

Using the Set-Variable cmdlet, we’ll make two modifications to our variable, at nearly the same time. First, we’ll update the Description property, so that it’s holding the original variable assignment (the string ‘first value’). In the same, Set-Variable command, we’ll modify the current value of the variable to the string ‘second value.’ Notice in the final below command, that our Description property has a value now, too.

PS > Set-Variable -Name x -Value 'second value' -Description $x
PS > $x
second value
PS > Get-Variable -Name x | Select-Object -Property *

Name        : x
Description : first value
Value       : second value
Visibility  : Public
Module      :
ModuleName  :
Options     : None
Attributes  : {}

In the next example, you’ll see how we can return the original value and the variable’s updated value. Beneath that, I’ve included a couple ways to compare these values. That gets back to what the person on Facebook was trying to determine. Again, this is fairly unconventional use of the Description property, but it does avoid the need for a second variable to hold the first variable’s original value. That said, one of my examples uses a second, comparison variable.

PS > (Get-Variable -Name x).Description
first value
PS > (Get-Variable -Name x).Value
second value
PS > $x
second value

PS > (Get-Variable -Name x).Description -ne (Get-Variable -Name x).Value
True
PS > (Get-Variable -Name x).Description -ne $x
True
PS > $y = (Get-Variable -Name x).Description
PS > $y -ne $x
True

In these last examples, we’re running into a bit of a problem, we are going to have to keep in mind (if anyone even dares use this approach). When we take a value that isn’t a string, and place it into the Description property, it becomes a string. That means, that when we take it back out, we’ll need to cast it back to its proper type. Take a look at the next series of examples for some assistance with this concept.

PS > $z = 5
PS > Set-Variable -Name z -Value 10 -Description $z
PS > Get-Variable -Name z | Select-Object -Property *

Name        : z
Description : 5
Value       : 10
Visibility  : Public
Module      :
ModuleName  :
Options     : None
Attributes  : {}

PS > (Get-Variable -Name z).Description | Get-Member |
>>> Select-Object -Property TypeName -Unique

TypeName
--------
System.String

PS > [int](Get-Variable -Name z).Description
5
PS > [int](Get-Variable -Name z).Description | Get-Member |
>>> Select-Object -Property TypeName -Unique

TypeName
--------
System.Int32

This goes for decimals values, too. If you use the Description property to store something that’s not a string, then you’re going to have to properly cast it when you’re taking it out, or you’re going to have a string. In these next examples, I used the GetType method to determine the value’s type, as opposed to Get-Member and Select-Object used above.

PS > $m = 2.5
PS > $m.GetType().Name
Double
PS > Set-Variable -Name m -Value 3.5 -Description $m
PS > $m
3.5
PS > $m.GetType().Name
Double
PS > (Get-Variable -Name m).Description
2.5
PS > (Get-Variable -Name m).Description.GetType().Name
String
PS > [double](Get-Variable -Name m).Description
2.5
PS > ([double](Get-Variable -Name m).Description).GetType().Name
Double

And that’s it. It’s may not be the first choice for saving a previous variable’s value, but it’s a choice. I rather liked the array option that was recommended; however, like the $Error array, I’d be tempted to put the newest/current value of a variable into index 0 and not at the end of the array. Anyway, back to real life now.

Add AccessKey and SecretKey Parameters to AWS Function

I’m spending more, and more, time with AWS. As a PowerShell enthusiast, I’ve therefore kept myself busy during a few of the week and weekend evenings. And sometimes during the day, too, when it makes sense.

As of today, I’ve written four AWS-specific functions for work. It’s everything from comparing my version of the AWSPowerShell module with the one in the PowerShell Gallery, returning the AWS service noun prefixes (EC2, CFN, etc.), and returning the EC2 instance type counts, and then there’s the newest one.

My most recent function can create an RDP (.rdp) file to connect to an EC2 instance (it gets the EC2’s IP address and places that in an RDP file). As I wrote up an email to share the function with a coworker—the one that mentioned Azure having an option to download RDP files—it occurred to me that none of my functions include an option to provide AccessKey and SecretKey parameters. All of them, that need it (two of them now), rely on persistent credentials having been saved.

Well, I’ll be going to go back through these two functions and add a way to accept an AccessKey and SecretKey parameter. First, however, was to figure out how. I literally typed “PowerShell add AccessKey and SecretKey to function” into Google (because I was being lazy,… and didn’t feel the need to write something that must surely be out there). Well, there wasn’t anything I could use, so I’ve done the work myself. Now, if anyone else types that, or something like that into Google or Bing, maybe they’ll get this answer.

Before I consider moving this to an existing function, I needed to make it work. I did just that, using parameter sets. I’ve long known about them, but haven’t really had a need for them in more than a few instances. This of course led me to view the parameter sets on an AWS cmdlet. Ugh, they aren’t even parameter sets on something like Get-EC2Instance. Try it yourself: Show-Command -Name Get-EC2Instance. I suspect there’s internal logic in the cmdlet itself to handle whether or not an AccessKey and SecretKey have been provided. Don’t have persistent, shell credentials: error*. Only provide an AccessKey: error*. Only provide a SecretKey: error*. Well, I went with parameter sets anyway.

* “No credentials specified or obtained from persisted/shell defaults.”

Here’s the complete function used to work through this desire. Take a look and continue reading about it further below.

Function Test-AWSParameterSets {
    [CmdletBinding(DefaultParameterSetName='NoAccessKeys')]
    Param(
        [Parameter(ParameterSetName='NoAccessKeys',Mandatory = $true)]
        [Parameter(ParameterSetName='AccessKeys',Mandatory=$true)]
        [String]$InstanceID,
 
        [Parameter(ParameterSetName='AccessKeys',Mandatory=$true)]
        [string]$AccessKey,
 
        [Parameter(ParameterSetName='AccessKeys',Mandatory=$true)]
        [string]$SecretKey

    )

    $PSBoundParameters
    '-----------------------------------------'
    "InstanceID: $InstanceID"
    "AccessKey: $AccessKey"
    "SecretKey: $SecretKey"
    "ParameterSet: $($PSCmdlet.ParameterSetName)"
}

In this first example, we’ll run the function without supplying any parameter names, or values. Because the InstanceID parameter is mandatory, and the NoAccessKeys parameter set is the default, the PowerShell engine prompts me to enter a value for InstanceID. After I entered i-1234554321, the function returns the $PSBoundParameters hash tables. This includes the parameter names and values that were supplied to the function at run time. Additionally, it returns the values for the InstanceID, AccessKey, SecretKey, and which of the two, parameter sets were used: NoAccessKeys or AccessKeys.

Test-AWSParameterSets
cmdlet Test-AWSParameterSets at command pipeline position 1
Supply values for the following parameters:
InstanceID: i-1234554321

Key        Value       
---        -----       
InstanceID i-1234554321
-----------------------------------------
InstanceID: i-1234554321
AccessKey: 
SecretKey: 
ParameterSet: NoAccessKeys

The next example includes an InstanceID parameter when the function is invoked. Therefore, I’m not prompted to enter any additional information. It produces the same output as it did above.

Test-AWSParameterSets -InstanceID i-1234554321
Key        Value       
---        -----       
InstanceID i-1234554321
-----------------------------------------
InstanceID: i-1234554321
AccessKey: 
SecretKey: 
ParameterSet: NoAccessKeys

The below example includes both an InstanceID parameter and an AccessKey parameter when the function is invoked. The moment the AccessKey parameter name was included, we switched to using the AccessKeys parameter set. Therefore, I was prompted to enter a SecretKey parameter value, as it’s a required parameter in that parameter set.

Test-AWSParameterSets -InstanceID i-1234554321 -AccessKey aacccceesssskkeeyy
cmdlet Test-AWSParameterSets at command pipeline position 1
Supply values for the following parameters:
SecretKey: ssseeecccrrreeetttkkkeeeyyy

Key        Value                      
---        -----                      
InstanceID i-1234554321               
AccessKey  aacccceesssskkeeyy         
SecretKey  ssseeecccrrreeetttkkkeeeyyy
-----------------------------------------
InstanceID: i-1234554321
AccessKey: aacccceesssskkeeyy
SecretKey: ssseeecccrrreeetttkkkeeeyyy
ParameterSet: AccessKeys

This is basically the opposite of the above example. I included the SecretKey parameter and it prompted me to enter the AccessKey parameter—we can’t have one without the other; they’re both mandatory in the AccessKeys parameter set.

Test-AWSParameterSets -InstanceID i-1234554321 -SecretKey ssseeecccrrreeetttkkkeeeyyy
cmdlet Test-AWSParameterSets at command pipeline position 1
Supply values for the following parameters:
AccessKey: aacccceesssskkeeyy

Key        Value                      
---        -----                      
InstanceID i-1234554321               
SecretKey  ssseeecccrrreeetttkkkeeeyyy
AccessKey  aacccceesssskkeeyy         
-----------------------------------------
InstanceID: i-1234554321
AccessKey: aacccceesssskkeeyy
SecretKey: ssseeecccrrreeetttkkkeeeyyy
ParameterSet: AccessKeys

The final example includes values for all the parameter names at the time the function is invoked. If someone wasn’t using a persistent set of credentials, then this is how you might expect an AWS function you’ve written to be used.

Test-AWSParameterSets -InstanceID i-1234554321 -AccessKey aacccceesssskkeeyy -SecretKey ssseeecccrrreeetttkkkeeeyyy
Key        Value                      
---        -----                      
InstanceID i-1234554321               
SecretKey  ssseeecccrrreeetttkkkeeeyyy
AccessKey  aacccceesssskkeeyy         
-----------------------------------------
InstanceID: i-1234554321
AccessKey: aacccceesssskkeeyy
SecretKey: ssseeecccrrreeetttkkkeeeyyy
ParameterSet: AccessKeys

Well, that’s the first part of the challenge. Now to incorporate what I’ve done here, into the functions that need it. Maybe, I’ll be back with that.

PSMonday #45: March 6, 2017

Topic: Reusable Code III

Notice: This post is a part of the PowerShell Monday series — a group of quick and easy to read mini lessons that briefly cover beginning and intermediate PowerShell topics. As a PowerShell enthusiast, this seemed like a beneficial way to ensure those around me at work were consistently learning new things about Windows PowerShell. At some point, I decided I would share these posts here, as well. Here’s the PowerShell Monday Table of Contents.

Back again.

We’ve made two new changes to the below code. One, we replaced powershell_ise, inside of our Get-Process command, with $Service. Two, we added a new, first line to our code. This command will prompt the user to enter a value. Read-Host will then assign that value to the $Service variable. That value will be used in the remainder of the PowerShell code every place $Service is found (there’s only the one). Now, the code is no longer only good for checking for the powershell_ise process; it’ll check for whatever process is entered.

$Service = Read-Host -Prompt 'Enter a Process Name'
    Get-Process -Name $Service |
        Select-Object -Property @{N='Process Name';E={$_.Name}},
        Description,
        Company,
        @{N='Shared Memory';E={"$([Math]::Round($_.WorkingSet / 1MB)) MB"}},
        @{N='Private Memory Size';
            E={"$([Math]::Round($_.PrivateMemorySize / 1MB)) MB"}}

If you were to open notepad, you could use this code to return its process information. So let’s do that; let’s say we have notepad open and we run our code.

Enter a Process Name:

Enter a Process Name: notepad

Process Name        : notepad
Description         : Notepad
Company             : Microsoft Corporation
Shared Memory       : 9 MB
Private Memory Size : 1 MB

Our few lines of code just became useable against any running process on the computer. To be as thorough as possible, we need to consider what happens when we enter a process that isn’t actually running — what’s it going to do? Let’s close notepad and find out.

Enter a Process Name: notepad

Get-Process : Cannot find a process with the name "notepad". Verify the process name and call the cmdlet again.
At line:3 char:1
+ Get-Process -Name $Service |
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo          : ObjectNotFound: (notepad:String) [Get-Process], ProcessCommandException
+ FullyQualifiedErrorId : NoProcessFoundForGivenName,Microsoft.PowerShell.Commands.GetProcessCommand

Get-Process doesn’t handle this gracefully, so let’s make a few more changes. We’ll first add a try-catch. The try portion will wrap the Get-Process and Select-Object commands. While it isn’t always necessary, we need to add the ErrorAction parameter name, with the Stop parameter value to Get-Process. This is required in order to prevent this error from being displayed. Forcing a terminating error isn’t always required, so don’t add it to commands when it’s not necessary. You try it without -ErrorAction. If it doesn’t work, you try -ErrorAction SilentlyContinue, and if that doesn’t work, you use -ErrorAction Stop, as we’ve done here.

The catch portion of our try-catch wraps a newly added, Write-Warning command that will gracefully indicate when a process isn’t running, or perhaps, just wasn’t spelled correctly.

$Service = Read-Host -Prompt 'Enter a Process Name'

try {
    Get-Process -Name $Service -ErrorAction Stop |
        Select-Object -Property @{N='Process Name';E={$_.Name}},
            Description,
            Company,
            @{N='Shared Memory';E={"$([Math]::Round($_.WorkingSet / 1MB)) MB"}},
            @{N='Private Memory Size';
                E={"$([Math]::Round($_.PrivateMemorySize / 1MB)) MB"}}
} catch {
    Write-Warning -Message "Cannot locate the $Service process."
}


Here’s what happens now, when you enter a process that isn’t running, or doesn’t even exist.

[powershell]
Enter a Process Name: notepad
WARNING: Cannot locate the notepad process.

Enter a Process Name: asdf
WARNING: Cannot locate the asdf process.

That’s it for this Monday. Keep paying attention; it’s about to get good.

Extract Media Folder from PowerPoint Files

On Wednesday, I wrote a response to something on Stack Overflow. So I don’t have to chase it down one day, I though I’d briefly discuss it here and include the updated code I would be more likely to use, providing I ever need to use it.

If you open a PowerPoint .pptx file in something like 7-Zip, you’ll quickly realize that the .pptx is a compressed, or archived, file format. While this brings down the file size (1.65MB file vs. 3.27MB when expanded), this is more more to say that there are a few folders, and several files, that make up a single PowerPoint file.

A person on Stack Overflow wanted to automate the expansion of a series of .pptx files and extract the media folder. This folder holds all the images in a PowerPoint file. I’m not going to bother to explain the code, but let me set the stage. I created a folder on my desktop called pptx. Inside the folder were four PowerPoint files using the .pptx file extension. My code created a new folder for each file in the pptx folder, expanded each PowerPoint file in their own new folders, located the nested media folder, moved it to the top level of their new folder, and then deleted all the other folders and files inside the new folder. If there wasn’t a media folder, it would still create the new folder; however, it would create a single text file in there called “No media folder.txt.” This was to help ensure the user didn’t think the code didn’t work, when they didn’t find the media folder inside the new folder.

I’ll included the original code from the forum post further below, but for now here’s an updated and cleaner version.

$Path = 'C:\users\tommymaynard\Desktop\pptx'
$Files = Get-ChildItem -Path $Path

Foreach ($File in $Files) {
    New-Item -Path $File.DirectoryName -ItemType Directory -Name $File.BaseName | Out-Null
    $NewFolder = $File.FullName.Split('.')[0]

    Add-Type -AssemblyName System.IO.Compression.FileSystem
    [System.IO.Compression.ZipFile]::ExtractToDirectory($File.FullName,$NewFolder)
    
    If (Test-Path -Path "$($NewFolder)\ppt\media") {
        Move-Item -Path "$($NewFolder)\ppt\media" -Destination $NewFolder
        Get-ChildItem -Path $NewFolder | Where-Object {$_.Name -ne 'media'} | Remove-Item -Recurse

    } Else {
        Get-ChildItem -Path $NewFolder | Remove-Item -Recurse
        New-Item -Path $NewFolder -ItemType File -Name 'No media folder.txt' | Out-Null
    }
}

And, here’s the original code and a link to the post itself. This below version didn’t use the $NewFolder variable, so there’s overwhelming inclusion of this piece of code: $File.FullName.Split(‘.’)[0]. It also defaulted to use the Expand-Archive cmdlet, if that was available. As I mentioned in the Stack Overflow post itself, it’s so slow. I’ve removed that so that it’s much faster now, which reminded me, I’ve actually written about this time difference before. It was just last October.

$Path = 'C:\users\tommymaynard\Desktop\pptx'
$Files = Get-ChildItem -Path $Path

Foreach ($File in $Files) {
    New-Item -Path $File.DirectoryName -ItemType Directory -Name $File.BaseName | Out-Null

    If (Get-Command -Name Expand-Archive) {
        Expand-Archive -Path $File.FullName -OutputPath $File.FullName.Split('.')[0]

    } Else {
        Add-Type -AssemblyName System.IO.Compression.FileSystem
        [System.IO.Compression.ZipFile]::ExtractToDirectory($File.FullName,$File.FullName.Split('.')[0])
    } # End If-Else.

    If (Test-Path -Path "$($File.FullName.Split('.')[0])\ppt\media") {
        Move-Item -Path "$($File.FullName.Split('.')[0])\ppt\media" -Destination $File.FullName.Split('.')[0]
        Get-ChildItem -Path $File.FullName.Split('.')[0] | Where-Object {$_.Name -ne 'media'} | Remove-Item -Recurse

    } Else {
        Get-ChildItem -Path $File.FullName.Split('.')[0] | Remove-Item -Recurse
        New-Item -Path $File.FullName.Split('.')[0] -ItemType File -Name 'No media folder.txt' | Out-Null
    } # End If-Else.
} # End Foreach.