PowerShell On Apple Mac OS X

Hello!

I love PowerShell. Its object-oriented nature is welcome relief from the endless string parsing of bash.

Good news! PowerShell Core installs on Windows, OS X, and Linux. I made it my default in Terminal on OS X. Here’s how.

I tested this on OS X Catalina (10.15) with PowerShell Core 7.

First, install from Homebrew like the docs say:

brew install powershell

Then open Terminal, select Terminal > Preferences in the menu, and set shells to open with /usr/local/bin/pwsh:

TerminalDefaultShell

Now quit and re-open Terminal. Boom! 💥 You’re standardized on PowerShell.

If you’re like me, though, you also need to add a bunch of stuff to your path. It’s similar to Linux-land, just update an environment variable in your profile, but there were gotchas.

PowerShell exposes an Environment Provider that works like a filesystem drive. That’s where your path is set:

PS /Users/adam> Get-Item Env:PATH
 
Name                           Value
----                           -----
PATH                           /usr/local/microsoft/powershell/7:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin

When I wrote this, the docs gave examples that used both Env:path and Env:Path, but neither worked on OS X. I had to use Env:PATH. It’s tricky because creating the wrong one doesn’t cause errors it just doesn’t do what you want.

The second gotcha was easier. In Windows the separator is ; so that’s what most examples use, but in OS X it’s :. I was copy/pasting from Windows code samples before I noticed the problem.

Just like Linux, modifications to Env:PATH are specific to your session. They’re lost on exit. We can make them permanent. First, find your PowerShell profile:

PS /Users/adam> $PROFILE
/Users/adam/.config/powershell/Microsoft.PowerShell_profile.ps1

Create that path and file if it doesn’t exist. Put this command in the Microsoft.PowerShell_profile.ps1 script:

$Env:PATH += ":/Users/adam/opt/path"

Now quit and re-open Terminal and your path should be up to date:

PS /Users/adam> Get-Item Env:PATH
 
Name                           Value
----                           -----
PATH                           /usr/local/microsoft/powershell/7:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/adam/opt/path

That’s it!

Happy automating,

Adam

Need more than just this article? We’re available to consult.

You might also want to check out these related articles:

PowerShell DSC: Stop On Errors

Hello!

I like my code to stop on errors, not continue past them. For example, if a script fails to enable logs for my app I want to know right away. I don’t want to find out tomorrow when I need logs that aren’t there. The earlier I know about errors, the better. I learned this as the Fail Early Fail Often pattern.

PowerShell has a dedicated type of error that’s non-terminating. Its default is to continue past them. That applies in DSC configurations, too.

We can see this with a simple DSC configuration and the Write-Error command in a Script resource:

Configuration Headless {
    Import-DscResource -ModuleName PSDesiredStateConfiguration
    Node 'localhost' {
        Log Before {
            Message = 'Before error.'
        }
        Script NonTerminatingError {
            GetScript = {@{Result = ''}}
            SetScript = {Write-Error 'Non-terminating error.'}
            TestScript = {Return $false}
        }
        Log After {
            Message = 'After error.'
        }
    }
}
 
Headless -ConfigurationData $ConfigurationData
Start-DscConfiguration -Wait -Force -Verbose -Path '.\Headless\'

The middle resource generates an error but the last resource still runs:

...
VERBOSE: [VAGRANT]:                            [[Log]Before] Before error.
VERBOSE: [VAGRANT]: LCM:  [ End    Set      ]  [[Log]Before]  in 0.0000 seconds.
VERBOSE: [VAGRANT]: LCM:  [ End    Resource ]  [[Log]Before]
VERBOSE: [VAGRANT]: LCM:  [ Start  Resource ]  [[Script]NonTerminatingError]
VERBOSE: [VAGRANT]: LCM:  [ Start  Test     ]  [[Script]NonTerminatingError]
VERBOSE: [VAGRANT]: LCM:  [ End    Test     ]  [[Script]NonTerminatingError]  in 0.0780 seconds.
VERBOSE: [VAGRANT]: LCM:  [ Start  Set      ]  [[Script]NonTerminatingError]
VERBOSE: [VAGRANT]:                            [[Script]NonTerminatingError] Performing the 
operation "Set-TargetResource" on target "Executing the SetScript with the user supplied 
credential".
VERBOSE: [VAGRANT]: LCM:  [ End    Set      ]  [[Script]NonTerminatingError]  in 0.0470 seconds.
VERBOSE: [VAGRANT]: LCM:  [ Start  Resource ]  [[Log]After]
VERBOSE: [VAGRANT]: LCM:  [ Start  Test     ]  [[Log]After]
VERBOSE: [VAGRANT]: LCM:  [ End    Test     ]  [[Log]After]  in 0.0000 seconds.
VERBOSE: [VAGRANT]: LCM:  [ Start  Set      ]  [[Log]After]
VERBOSE: [VAGRANT]:                            [[Log]After] After error.
...

We can tell DSC to stop on non-terminating errors by passing the ErrorAction flag when we start our configuration:

Start-DscConfiguration -Wait -Force -Verbose -ErrorAction 'Stop' -Path '.\Headless\'

Now it stops on the Write-Error:

VERBOSE: [VAGRANT]:                            [[Log]Before] Before error.
VERBOSE: [VAGRANT]: LCM:  [ End    Set      ]  [[Log]Before]  in 0.0000 seconds.
VERBOSE: [VAGRANT]: LCM:  [ End    Resource ]  [[Log]Before]
VERBOSE: [VAGRANT]: LCM:  [ Start  Resource ]  [[Script]NonTerminatingError]
VERBOSE: [VAGRANT]: LCM:  [ Start  Test     ]  [[Script]NonTerminatingError]
VERBOSE: [VAGRANT]: LCM:  [ End    Test     ]  [[Script]NonTerminatingError]  in 0.0780 seconds.
VERBOSE: [VAGRANT]: LCM:  [ Start  Set      ]  [[Script]NonTerminatingError]
VERBOSE: [VAGRANT]:                            [[Script]NonTerminatingError] Performing the 
operation "Set-TargetResource" on target "Executing the SetScript with the user supplied 
credential".
 
Stderr from the command:
 
powershell.exe : Non-terminating error.

There’s also an $ErrorActionPreference variable, but that didn’t work no matter where I set it. The ErrorAction flag seems to be the way.

This isn’t necessarily a best practice, the PowerShell default is to continue past errors, but I usually have a better life if I switch it to stop.

Happy automating!

Adam

Need more than just this article? We’re available to consult.

You might also want to check out these related articles:

PowerShell DSC: Self Signed SSL Certs

Hello!

First, this isn’t a best practices guide for SSL certificates, it’s a how-to for creating functional ones. As always, only use self-signed certs when you’ve specifically validated that they’re a sufficiently secure solution.

When I do need self-signed certs and I’m working in Windows, I generate them with PowerShell DSC and its Script Resource. It works great for my cases. There are more robust ways that may also be worth looking at, like Custom Resources.

I always need certs with no password to make it easy to start apps unattended, so that’s what these instructions create.

This assumes you’ve already installed OpenSSL. I use Chocolatey.

Pre-requisites out of the way. Now, the code:

Script SelfSignedCert {
    GetScript = {@{Result = ''}}
    SetScript = {
        New-Item -ItemType Directory -Force -Path 'C:\Tmp\Ssl\'
 
        # Generate PFX with a temporary password
        $Cert = New-SelfSignedCertificate `
            -CertStoreLocation 'cert:\localmachine\my' `
            -DnsName 'localhost'
        $Password = ConvertTo-SecureString `
            -String 'temppass' `
            -Force `
            -AsPlainText
        Export-PfxCertificate `
            -Cert "cert:\localmachine\my\$($Cert.Thumbprint)" `
            -FilePath 'C:\Tmp\Ssl\Cert.pfx' `
            -Password $Password
 
        # Convert PFX to Key/PEM with no password
        C:\Program` Files\OpenSSL-Win64\bin\openssl.exe pkcs12 `
            -in 'C:\Tmp\Ssl\Cert.pfx' `
            -nocerts `
            -nodes `
            -out 'C:\Tmp\Ssl\Pkcs12.pem' `
            -passin 'pass:temppass'
        # 'openssl.exe rsa' sends 'writing RSA key' to the error stream
        # on success. We have to redirect that output or the Script
        # resource errors.
        C:\Program` Files\OpenSSL-Win64\bin\openssl.exe rsa `
            -in 'C:\Tmp\Ssl\Pkcs12.pem' `
            -out 'C:\Tmp\Ssl\Rsa.key' `
            2> Out-Null
        C:\Program` Files\OpenSSL-Win64\bin\openssl.exe pkcs12 `
            -clcerts `
            -in 'C:\Tmp\Ssl\Cert.pfx' `
            -nokeys `
            -out 'C:\Tmp\Ssl\Cert.pem' `
            -passin 'pass:temppass'
 
        # Clean up leftovers from the conversion
        Remove-Item 'C:\Tmp\Ssl\Cert.pfx'
        Remove-Item 'C:\Tmp\Ssl\Pkcs12.pem'
    }
    TestScript = {Test-Path 'C:\Tmp\Ssl'}
}

Line 32 (highlighted) is the tricky one. The script resource fails when its code outputs to the error stream. OpenSSL’s rsa command sends the string "writing RSA key" to the error stream when it succeeds. So we get failures like these:

Stderr from the command:
 
powershell.exe : writing RSA key
    + CategoryInfo          : NotSpecified: (writing RSA key:String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError
    + CategoryInfo          : NotSpecified: (writing RSA key:) [], CimException
    + FullyQualifiedErrorId : NativeCommandError
    + PSComputerName        : localhost

I bet the reason is this: OpenSSL was designed for Linux. In Linux, it’s common to send informational output to stderr. That keeps it out of stdout and therefor keeps it from passing to other apps via pipes (|). In PowerShell, there are many streams, including a dedicated one for informative output (the “verbose” stream). That makes it an anti-pattern to send informative output to PowerShell’s error stream; you should use the verbose stream. So it makes sense for DSC to assume that anything on the error stream is a real error, unlike in Linux. The person who ported OpenSSL didn’t account for this.

The only workaround I could find was to redirect the error stream to Out-Null. OpenSSL didn’t recognize the ErrorAction flag.

I couldn’t reproduce this behavior in raw PowerShell, only in the DSC script resource. If you know the details on why that is, I’d love to hear from you.

Hope this helps! Happy automating,

Adam

Need more than just this article? We’re available to consult.

You might also want to check out these related articles:

PowerShell DSC EXE ProductID And Name

Hello!

PowerShell DSC can install EXEs with its Package resource:

Package MyPackage {
    Name = [string]
    Path = [string]
    ProductId = [string]
}

Path is easy, it’s just the path to the installer. But what goes in Name or ProductID?

Let’s use WinRAR as an example. You shouldn’t actually install this with DSC, you should use Chocolatey, but I needed something to demo.

There are lots of instructions for finding the product ID for MSIs, like Sam Cogan’s, but they don’t work for EXEs. After reading tons of sources that weren’t solid enough to link, I figured out that EXEs don’t have a product ID. Just the empty string: ''.

The name was trickier. For WinRAR it’s WinRAR, right? Nope. DSC throws errors:

powershell.exe : PowerShell DSC resource MSFT_PackageResource  failed to execute Set-TargetResource functionality 
    + CategoryInfo          : NotSpecified: (PowerShell DSC ... functionality :String) [], RemoteException
    + FullyQualifiedErrorId : NativeCommandError
with error message: Package from C:\winrar-x64-561.exe was installed, but the specified ProductId 
and/or Name does not match package details 
    + CategoryInfo          : InvalidOperation: (:) [], CimException
    + FullyQualifiedErrorId : ProviderOperationExecutionFailure
    + PSComputerName        : localhost

Turns out you can find the name DSC needs by doing a temp install of the app by hand (I used a Vagrant box) and then checking its entry in “Add or remove programs” in the system settings:

WinRARName

The Name is the full WinRAR 5.61 (64-bit), including the spaces and special characters.

Here’s the full DSC resource:

Package WinRAR {
    Name = 'WinRAR 5.61 (64-bit)'
    Path = "$Env:SystemDrive\winrar-x64-561.exe"
    ProductId = ''
    Arguments = '/S'
}

(The /S stops the installer from asking for user input)

That’s it! Use empty string for the product ID and look up the name in “Add or remove programs” and you’re good to go.

Happy automating,

Adam

Need more than just this article? We’re available to consult.

You might also want to check out these related articles:

PowerShell DSC In Vagrant

I work mostly on Apple Mac OS X. I’ve also been writing a lot of Windows automation, and that means PowerShell DSC. PSDSC doesn’t work on OS X yet, and even once it does I won’t be able to test Windows-only resources. To test my configurations, I use Vagrant boxes. It took a little fiddling to get this set up, so I’m posting the code here in case you’re in the same situation.

This assumes you already have Vagrant working. I use the VirtualBox provider and I install everything with homebrew.

Today, Vagrant doesn’t have a native PowerShell DSC provisioner. I use the shell provisioner to run my Headless PSDSC script, which compiles and runs a configuration (headless_dsc.ps1):

Param(
    [Parameter(Mandatory=$false)]
    [String[]]
    $InputXmlFile
)
  
if ($InputXmlFile) {
    [XML]$Inputs = Get-Content -Path $InputXmlFile
}
  
$NodeData = @{
    NodeName = "localhost"
    Message = "This was hardcoded in the node data."
    Inputs = $Inputs.inputs
}
$ConfigurationData = @{AllNodes = @($NodeData)}
  
Configuration Headless {
    Import-DscResource -ModuleName PSDesiredStateConfiguration
  
    Node 'localhost' {
        Log Message {
            Message = $AllNodes.Message
        }
        if ($AllNodes.Inputs) {
            Log Input {
                Message = $AllNodes.Inputs.Message
            }
        }
    }
}
  
Headless -ConfigurationData $ConfigurationData
Start-DscConfiguration -Wait -Force -Verbose -Path .\Headless\

And here’s an input file to provide the example messages (input.xml):

<Inputs>
    <Message>This message came from the XML inputs.</Message>
</Inputs>

Now all you need is a Vagrantfile in the same directory:

Vagrant.configure("2") do |config|
  config.vm.box = "StefanScherer/windows_2019"
  config.vm.provision "file" do |file|
    file.source = "input.xml"
    file.destination = "input.xml"
  end
  config.vm.provision "shell" do |shell|
    shell.path = "headless_dsc.ps1"
    shell.args = ["-InputXmlFile", "input.xml"]
 
    # https://github.com/hashicorp/vagrant/issues/9138#issuecomment-444408251
    shell.privileged = false
  end
end

To create the box and run PSDSC:

vagrant up

If you’ve made changes and you want to recomplie and rerun the configuration:

vagrant provision

If you need to connect to the instance and run commands, check out vagrant rdp. You’ll need to install Remote Desktop from the App Store. There’s a funky bug: if Remote Desktop isn’t open, the first rdp command will open the app but won’t start a session. Just run it again and you’ll get a session window.

The provisioner uploads the script and XML file on every run even though the current directory is mounted as a synced folder on the box by default. I could have used an inline shell script to call the script directly via that sync, but sometimes I disable the sync for testing and it’s convenient for the provisioner to keep working.

Without shell.privileged = false, I got errors like this:

default: (10,8):UserId:
default: At line:72 char:1
default: + $folder.RegisterTaskDefinition($task_name, $task, 6, $username, $pass ...
default: + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
default:     + CategoryInfo          : OperationStopped: (:) [], ArgumentException
default:     + FullyQualifiedErrorId : System.ArgumentException
default: The system cannot find the file specified. (Exception from HRESULT: 0x80070002)
default: At line:74 char:1
default: + $registered_task = $folder.GetTask("\$task_name")
default: + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
default:     + CategoryInfo          : OperationStopped: (:) [], FileNotFoundException
default:     + FullyQualifiedErrorId : System.IO.FileNotFoundException
default: You cannot call a method on a null-valued expression.
default: At line:75 char:1
default: + $registered_task.Run($null) | Out-Null
default: + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
default:     + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
default:     + FullyQualifiedErrorId : InvokeMethodOnNull

The whole process froze and I had to ctrl+c twice to close it. There’s a bug. I left a link in a comment in the Vagrantfile that I recommend you keep in your code so this setting isn’t confusing later.

Happy automating!

Adam

Need more than just this article? We’re available to consult.

You might also want to check out these related articles:

PowerShell Install-Module: Use Install-Package Instead

Hello!

When I restarted in the Windows ecosystem, I was installing PowerShell modules like this:

Install-Module 'PSDscResources'

This is similar to installing a Python package with pip in Linux:

pip install ansible

Install-Module installs PSDscResources from the PowerShell Gallery. Pip installs Ansible from PyPI.

Like the Linux ecosystem, the Windows ecosystem has several package databases. The PS Gallery I linked above. Chocolatey Packages. The NuGet Gallery. They each host different kinds of software, similar to how you can install different kinds of software from PyPI, Ruby Gems, and yum/apt.

In Linux, you work with each system separately from the others. In Windows, you can work with them separately, but you usually shouldn’t. Microsoft built PackageManagement (formerly OneGet) to orchestrate the different package repositories. Now, each source of packages is implemented in a “Provider”. There’s a provider for the PS Gallery (called PowerShellGet) and another for Chocolatey. PackageManagement gives you a single interface to all of them through one set of cmdlets. Check out their architecture diagram.

So, we don’t need to use Install-Module to work directly with PS Gallery. Instead, we can just ask PackageManagement to install what we need and it’ll figure out where to get it from:

Install-Package 'PSDscResources'

You can also do things like check where an installed package came from:

Get-Package 'PSDscResources'
 
Name                           Version          Source                           ProviderName
----                           -------          ------                           ------------
PSDscResources                 2.12.0.0         https://www.powershellgallery.c… PowerShellGet

In Linux, work with each package manager separately. In Windows, just use PackageManagement.

Happy automating!

Adam

Need more than just this article? We’re available to consult.

You might also want to check out these related articles:

Headless PowerShell DSC Script

Hello!

In clouds, “headless” deployment means instances provision themselves when they start. There’s no external server infrastructure orchestrating their config, everything they need to do they do on their own. This is the most common deployment pattern I’ve seen in DevOps.

It took me some fiddling to get this pattern set up in PowerShell DSC for my situation, so I’m posting the code in case you’re running in to the same thing. It’s a single script that compiles and runs your configuration, optionally reading inputs from an XML file.

First, the code:

Param(
    [Parameter(Mandatory=$false)]
    [String[]]
    $InputXmlFile
)
 
if ($InputXmlFile) {
    [XML]$Inputs = Get-Content -Path $InputXmlFile
}
 
$NodeData = @{
    NodeName = "localhost"
    Message = "This was hardcoded in the node data."
    Inputs = $Inputs.inputs
}
$ConfigurationData = @{AllNodes = @($NodeData)}
 
Configuration Headless {
    Import-DscResource -ModuleName PSDesiredStateConfiguration
 
    Node 'localhost' {
        Log Message {
            Message = $AllNodes.Message
        }
        if ($AllNodes.Inputs) {
            Log Input {
                Message = $AllNodes.Inputs.Message
            }
        }
    }
}
 
Headless -ConfigurationData $ConfigurationData
Start-DscConfiguration -Wait -Force -Verbose -Path .\Headless\

A sample XML file:

<Inputs>
    <Message>This message came from the XML inputs.</Message>
</Inputs>

And the script output on a Windows Server 2019 Vagrant box:

PS C:\vagrant> .\headless_dsc.ps1 -InputXmlFile .\input.xml
 
    Directory: C:\vagrant\Headless
 
Mode                LastWriteTime         Length Name
----                -------------         ------ ----
------       11/11/2019   8:21 AM           2488 localhost.mof
VERBOSE: Perform operation 'Invoke CimMethod' with following parameters, ''methodName' =
SendConfigurationApply,'className' = MSFT_DSCLocalConfigurationManager,'namespaceName' =
root/Microsoft/Windows/DesiredStateConfiguration'.
VERBOSE: An LCM method call arrived from computer VAGRANT with user sid S-1-5-21-1529561135-2561037041-51718299-1000.
VERBOSE: [VAGRANT]: LCM:  [ Start  Set      ]
VERBOSE: [VAGRANT]: LCM:  [ Start  Resource ]  [[Log]Message]
VERBOSE: [VAGRANT]: LCM:  [ Start  Test     ]  [[Log]Message]
VERBOSE: [VAGRANT]: LCM:  [ End    Test     ]  [[Log]Message]  in 0.0000 seconds.
VERBOSE: [VAGRANT]: LCM:  [ Start  Set      ]  [[Log]Message]
VERBOSE: [VAGRANT]:                            [[Log]Message] This was hardcoded in the node data.
VERBOSE: [VAGRANT]: LCM:  [ End    Set      ]  [[Log]Message]  in 0.0000 seconds.
VERBOSE: [VAGRANT]: LCM:  [ End    Resource ]  [[Log]Message]
VERBOSE: [VAGRANT]: LCM:  [ Start  Resource ]  [[Log]Input]
VERBOSE: [VAGRANT]: LCM:  [ Start  Test     ]  [[Log]Input]
VERBOSE: [VAGRANT]: LCM:  [ End    Test     ]  [[Log]Input]  in 0.0000 seconds.
VERBOSE: [VAGRANT]: LCM:  [ Start  Set      ]  [[Log]Input]
VERBOSE: [VAGRANT]:                            [[Log]Input] This message came from the XML inputs.
VERBOSE: [VAGRANT]: LCM:  [ End    Set      ]  [[Log]Input]  in 0.0000 seconds.
VERBOSE: [VAGRANT]: LCM:  [ End    Resource ]  [[Log]Input]
VERBOSE: [VAGRANT]: LCM:  [ End    Set      ]
VERBOSE: [VAGRANT]: LCM:  [ End    Set      ]    in  0.0940 seconds.
VERBOSE: Operation 'Invoke CimMethod' complete.
VERBOSE: Time taken for configuration job to complete is 0.372 seconds

Some notes:

  • There are several modules that implement DSC resources. I start with PSDesiredStateConfiguration and then upgrade if I need to. Check out this article if you’re getting errors defining resources or their properties.
  • This uses Log resources to demonstrate functionality and to show you how to access the ConfigurationData. You should replace those in your config.
  • You don’t have to use ConfigurationData to pass the inputs. You’ll see other patterns using regular function params, for example. I used ConfigurationData because it’s DSC’s built-in method and I use the native approach unless I have a specific reason to use something else.
  • I used XML for the inputs because PowerShell supports XML natively. It also supports JSON, but JSON makes you escape backslashes \ and since those are common path characters in Windows it makes for ugly input strings. Plus XML supports comments and JSON doesn’t and in DevOps you very often need to add comments to your input choices.
  • This is written to run directly as a PowerShell script because that’s what I needed at the time. If you’re using Azure Resource Manager Template, check out its native integration.
  • This assumes you always want verbose output. DSC is a quiet tool by default and I prefer to make it verbose. You may want to parameterize or consider using CmdletBinding.
  • This assumes you’ll only ever define one node: localhost. If you adjust that you’ll need to update the node references.

Happy configuring!

Adam

Need more than just this article? We’re available to consult.

You might also want to check out these related articles:

PowerShell: Getting Properties From Objects In Arrays

In PowerShell, I often run commands that return arrays I need to filter. Once I’ve filtered out object I’m looking for, I need to read a property off that object. There are a few ways to do this. Here are three.

These are also good examples if you’re new to PowerShell and trying to switch from the Linux mindset of parsing strings to the PowerShell mindset of manipulating objects.

For these examples we’ll be using an array filtered from Get-Process:

Get-Process | Where-Object ProcessName -Match "update"
 
 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
      0     0.00      10.32     380.46     733   1 SoftwareUpdateN

Method 1: Select-Object

Select-Object reads properties from objects. We can pipe it the object we’ve found:

Get-Process | Where-Object ProcessName -Match "update" | Select-Object cpu
 
       CPU
       ---
380.761615

In this case, Where-Object returns one object and Select-Object reads the property off of it, but this still works if we match multiple processes. Then, Where-Object returns an array that gets unpacked by PowerShell’s pipeline and sent to Select-Object one at a time.

This is basically a property filter. It still returns an array of Process objects, but those objects only have the one property you selected. We can see this with Get-Member:

Get-Process | Where-Object ProcessName -Match "update" | Select-Object cpu | Get-Member
 
   TypeName: Selected.System.Diagnostics.Process
 
Name        MemberType   Definition
----        ----------   ----------
Equals      Method       bool Equals(System.Object obj)
GetHashCode Method       int GetHashCode()
GetType     Method       type GetType()
ToString    Method       string ToString()
CPU         NoteProperty System.Double CPU=19.3147851

Four methods, but only one property.

Method 2: Subexpression

If we capture the output of our match command into a subexpression, we can access the object’s properties using dotted notation like we would with any variable:

$(Get-Process | Where-Object ProcessName -Match "update").cpu
380.761615

This also works if our match returned multiple objects. The subexpression will contain an array and PowerShell will automatically get the cpu property from each object in that array.

Unlike the filter of Select-Object, this doesn’t return Process objects with a cpu property. Instead, it returns a double (number) with the actual value:

$(Get-Process | Where-Object ProcessName -Match "update").cpu.GetType()
 
IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Double                                   System.ValueType

Method 3: Loop

Like any output, we can pipe our match into a ForEach-Object loop and use the $_ variable to access properties of each item the loop sees:

Get-Process | Where-Object ProcessName -Match "update" | ForEach-Object {$_.cpu}
381.625785

The loop will of course work on multiple objects.

Just like the subexpression, this returns the actual value instead of an object with one property.

Happy automating!

Adam

Need more than just this article? We’re available to consult.

You might also want to check out these related articles:

How to Use Out-String in PowerShell: Don’t

In my PowerShell Help Commands For Linux Users post, I showed you this pattern for searching for command aliases:

Get-Alias | Out-String -stream | Select-String -Pattern 'move'

A beginner mistake! Here’s the problem:

I’m used to the Unix shells, like bash, where everything is a string. When you run alias in bash, you get this:

$ alias
alias hello='echo hello'
alias la='ls -a'
alias ll='ls -l'

A multiline string with one alias per line. You search those aliases like this:

$ alias | grep hello
alias hello='echo hello'

grep does a pattern match on each string of those lines, one line at a time.

PowerShell is object-oriented. Its Get-Alias command doesn’t return a multiline string, it returns an array of objects. Those object have properties like Name. If you want to find aliases whose name match a pattern, you just iterate the array:

Get-Alias | Where-Object Name -Match ".*move.*"

Where-Object checks the Name property of each object in the array to see if it matches the .*move.* pattern. (Technically Where-Object isn’t doing the iteration, PowerShell is unpacking the array and sending the objects through the pipe one at a time)

This is so much better. It’s like writing Python. Standard data types and logic.

In my past post, I piped to Out-String, which converts objects into strings. That allowed me to imitate the Linux pattern by searching with Select-String (basically the PowerShell grep). Totally unnecessary. In PowerShell you can just match on object properties directly. You don’t need to cast to strings.

In a simple case like this PowerShell’s object-oriented nature is mostly a novelty, but in more complex cases it ends up being hugely powerful. More posts coming!

Happy automating,

Adam

Need more than just this article? We’re available to consult.

You might also want to check out these related articles:

PowerShell Help Commands For Linux Users

Hello!

Microsoft’s Azure keeps growing. Azure isn’t all Windows, but Windows is a great reason to use Azure and a lot of Windows workloads get run there. In today’s Windows, PowerShell is the engineer’s interface to the OS and is often the language behind automation. I decided I needed to know PowerShell to keep up with the industry.

You can run PowerShell on OSX! There are a few differences (e.g. it’s still running inside of the Terminal app so things like ctrl+u work, but in native PowerShell on Windows they don’t), but the basic system is the same. I’ve been running it day to day so I can get used to it.

In bash, there are a bunch of commands I use when I’m trying to figure things out. Like searching the shell aliases or the man pages database. I found plenty of great guides on the basics of PowerShell, but it took some fiddling to find all the parallel pwsh commands for my “figuring things out” commands. I also found a couple handy commands that don’t exist directly in bash (that I know of). Here’s a table:

Update 2019-11: The Out-String uses here are anti-patterns. See my newer post on why. I’ve updated this table with the better way.

Bash PowerShell
man cd
Get-Help cd
man -k 'search string'
Get-Help 'search string'
alias
Get-Alias
Not available. Searches for aliases of a specific cmdlet (‘out-host’ in this example).
Get-Alias -Definition 'out-host'
alias | grep move
Bad:

Get-Alias | Out-String -stream | Select-String -Pattern 'move'

Good:

Get-Alias | Where-Object Name -Match ".*move.*"
env
Get-ChildItem env:
env | grep PATH
Bad:

Get-ChildItem env: | Out-String -stream | sls -Pattern 'PATH'

Good:

Get-ChildItem env: | Where-Object Name -Match ".*PATH.*"
Not available. Wildcard-matches environment variable names but not values (‘PATH’ in this example).
Get-ChildItem env:*PATH*

One other tip is that PowerShell has great tab-completion for commands. It also follows a consistent Verb-Noun convention, so you can try to guess the verb and tab-complete your way to the command. Like if you wanted to know the time, try “get” and hit tab twice and you’ll get a list that has “Get-Date” in it.

Hope that saves you some time!

Adam

Need more than just this article? We’re available to consult.

You might also want to check out these related articles: