Commandlet with Parameter accepting ByPropertyName works for one parameter but not for other parameter - powershell-7.0

Get-Command Accepts both Module and Name using ByPropertyName
help Get-Command -Parameter *
-Module <System.String[]>
Position? named
Accept pipeline input? True (ByPropertyName)
-Name <System.String[]>
Position? 0
Accept pipeline input? True (ByPropertyName, ByValue)
However
[PSCustomObject]#{Name = 'dir'} | Get-command /*This Works*/
but
[PSCustomObject]#{Module = 'Microsoft.PowerShell.Archive'} | Get-command /*This is NOT Working*/

The byvalue of name screws it up because of its higher precedence over the pipe. Here's a workaround, * for the name:
[pscustomobject]#{module = 'Microsoft.PowerShell.Archive'} | get-command -name *
CommandType Name Version Source
----------- ---- ------- ------
Function Compress-Archive 1.2.5 Microsoft.PowerShell.Archive
Function Expand-Archive 1.2.5 Microsoft.PowerShell.Archive
Even this way name is still required.
'Microsoft.PowerShell.Archive' | get-command -module { $_ } -name *

Related

Output debug information from pester test

I'm writing a system in C# that has lots of features to test. One feature is a set of PowerShell custom cmdlets for managing my system.
I'm using pester (v5) to test my cmdlets and will have a lot of test cases for the many scenarios that I want to test.
I can use pester successfully but I can't get any debug output from the tests - only the test report. The test report is fine to see what's passing and failing but if I have a failure then I want to be able to output results that can give me the context of what is going wrong.
I've tried all the settings I can think of for PesterConfiguration to no avail, e.g.
$PesterPreference = [PesterConfiguration]::Default
#$PesterPreference.Output.Verbosity = "Diagnostic"
$PesterPreference.Debug.WriteDebugMessages = $true
#$PesterPreference.Debug.WriteDebugMessagesFrom = "*"
As an example, imagine I have a get-foo cmdlet that returns an object with a Thing property that should have a value of "Bar".
I can write a pester test to check this but I would like to be able to output the returned object as part of the test to see something like:
Name Value
---- -----
Thing Bar
Other 124
Describe 'Test My CmdLet' {
It 'get-foo should return bar' {
$obj = get-foo
write-output $obj
$obj | Should -Not -BeNullOrEmpty
$obj.Thing | Should -Be "Bar"
}
}
Without the ability to get output then I have to reconstruct the test setup and structure outside of pester directly in PowerShell - which is a painful process.
If anyone has any advice on how to get my output then I'd appreciate the advice.
Thanks,
David
The answer seems to be to set the $VerbosePreference="Continue" in the script
E.g.
$VerbosePreference = "Continue"
Describe 'Test My CmdLet' {
It 'get-foo should return bar' {
$obj = get-foo
$obj | out-string | write-verbose
$obj | Should -Not -BeNullOrEmpty
$obj.Thing | Should -Be "Bar"
}
}
Will dump out the detail of $obj as expected. This also works with $DebugPreference, $InformationPeference etc.
You can access StandardOutput per test by using the result-object you get with -PassThru. Demo:
# Just using a container here to avoid creating a testfile
$container = New-PesterContainer -ScriptBlock {
Describe 'Test My CmdLet' {
It 'get-foo should return bar' {
function get-foo { 1..3 | % { "hello $_" } }
$obj = get-foo
write-output $obj
$obj | Should -Not -BeNullOrEmpty
}
}
}
$pesterResult = Invoke-Pester -Container $container -PassThru
$pesterResult.tests | Format-Table ExpandedPath, StandardOutput
ExpandedPath StandardOutput
------------ --------------
Test My CmdLet.get-foo should return bar {hello 1, hello 2, hello 3}
I couldn't find any configuration option to show the same I would see on the console so I'm using the PassThru and a for loop to print all the tests output afterwards:
$(Invoke-pester -PassThru).Tests | ForEach-Object -Process {"-"*80;$_.Result + ": " + $_.Name;"-"*80;$_.StandardOutput}

How can I invoke a scriptblock in the caller's context?

Consider the following call site:
$modifiedLocal = 'original local value'
'input object' | SomeScriptblockInvoker {
$modifiedLocal = 'modified local value'
[pscustomobject] #{
Local = $local
DollarBar = $_
}
}
$modifiedLocal
I would like to implement SomeScriptblockInvoker such that
it is defined in a module, and
the scriptblock is invoked in the caller's context.
The output of the function at the call site would be the following:
Local DollarBar
----- ---------
local input object
modified local value
PowerShell seems to be capable of doing this. For example replacing SomeScriptblockInvoker with ForEach-Object yields exactly the desired output.
I have come close using the following definition:
New-Module m {
function SomeScriptblockInvoker {
param
(
[Parameter(Position = 1)]
[scriptblock]
$Scriptblock,
[Parameter(ValueFromPipeline)]
$InputObject
)
process
{
$InputObject | . $Scriptblock
}
}
} |
Import-Module
The output of the call site using that definition is the following:
Local DollarBar
----- ---------
local
modified local value
Note that DollarBar is empty when it should be input object.
(gist of Pester tests to check for correct behavior)
In general, you can't. The caller of a scriptblock does not have control over the SessionState associated with that scriptblock and that SessionState determines (in part) the context in which a scriptblock is executed (see the Scope section for details). Depending on where the scriptblock is defined, its SessionState might match the caller's context, but it might not.
Scope
With respect to the context in which the scriptblock is executed, there are two related considerations:
The SessionState associated with the scriptblock.
Whether or not the calling method adds a scope to the SessionState's scope stack.
Here is a good explanation of how this works.
The $_ Automatic Variable
$_ contains the current object in the pipeline. The scriptblock provided to % is interpreted differently from the scriptblock provided . and &:
'input_object' | % {$_} - The value of $_ is 'input_object' because the scriptblock is bound to %'s -Process parameter. That scriptblock is executed once for each object in the pipeline.
'input_object' | . {process{$_}} and 'input_object' | & {process{$_}} - The value of $_ is 'input_object' because the $_ in the scriptblock is inside a process{} block which executes once for each object in the pipeline.
Note that in the OP $_ was empty when the scriptblock was invoked using .. This is because the scriptblock contained no process{} block. Each of the statements in the scriptblock were implicitly part of the scriptblock's end{} block. By the time an end{} block is run, there is no longer any object in the pipeline and $_ is null.
. vs & vs %
., &, and % each invoke the scriptblock using the SessionState of the scriptblock with some differences according to the following table:
+---+-----------------+-----------+-------------------+----------------------+
| | Name | Kind | interprets {} as | adds scope to stack |
+---+-----------------+-----------+-------------------+----------------------+
| % | ForEach-Object | Command | Process block | No |
| . | dot-source | Operator | scriptblock | No |
| & | call | Operator | scriptblock | Yes |
+---+-----------------+-----------+-------------------+----------------------+
The % command has other parameters corresponding to Begin{} and End{} blocks.
For variable assignments made by the scriptblock to have side-effects on the SessionState associated with the scriptblock, use an invocation method that does not add a scope to the stack. Otherwise, the variable assignment will only affect the newly-created scope and disappear once the scriptblock completes execution.
The Most Viable Options
The two most viable options for passing the tests in OP are as follows. Note that neither invokes the scriptblock strictly in the caller's context but rather in a context using the SessionState associated with the scriptblock.
Option 1
Change the call site so that the scriptblock includes process{}:
$modifiedLocal = 'original local value'
'input object' | SomeScriptblockInvoker {
process {
$modifiedLocal = 'modified local value'
[pscustomobject] #{
Local = $local
DollarBar = $_
}
}
}
$modifiedLocal
And invoke the scriptblock using SomeScriptblockInvoker in OP.
Option 2
Invoke the scriptblock using % as suggested by PetSerAl.

Powershell: Forcing evaluation of a block of code in Add-member's -value option

I'm trying to import users into an active directory using a CSV file and a powershell script. I create a CSV with the headers normally associated with an AD object:
mail,name,givenName,middleName,surname,company,department,title,plaintextPassword,path,description,userPrincipalName
...and filled it up.
Now I want to use Powershell's new-aduser cmmdlet to generate users for each item in this sheet - the problem I'm having is that new-aduser requires a SecureString, not just a normal string for an account's password. Skipping this conversion results in my users being created correctly, but with no passwords and their account disabled.
The command I'm using is as follows:
import-csv .\users.csv | add-member -passthru -memberType NoteProperty -value {$_ | select plaintextPassword | ConvertTo-SecureString -fromplaintext -force}
The result is user records like the following:
mail : tom.fubar#contoso.com
name : tom.fubar
givenName : Tom
middleName :
surname : Fubar
company : Contoso
department : IT
title : Technician
accountPassword : LongPasswordThatFitsADComplexityRequirements123!
path : OU=UserAccounts,OU=IT,OU=employees,DC=contoso,DC=com
description :
userPrincipalName : tom.fubar#contoso.com
encodedPassword : {$_ | select accountPassword | ConvertTo-SecureString -asplaintext -force}
The bit of code that should be evaluated for converting the plaintext password to a SecureString is being passed verbatim, rather than executed inline.
What is the proper way to force the code block to be evaluated, and use its result as the argument to New-Member -value?
Tried:
Enclosing the script block in $(...) - Results in a null NoteProperty added to the object
Replacing the {...} with $(...) - Results in a null NoteProperty added to the object
(as shown by piping the whole command to Get-Member)
Eris has posted a perfectly valid workaround, but to answer why it won't work for you, it's because $_ doesn't apply to any old script block. It's "special" and used only in certain contexts.
Additionally, -NotePropertyValue expects a static value.
Instead, you could add a ScriptProperty like so:
import-csv .\users.csv |
add-member -passthru -memberType ScriptProperty -value {$this.plaintextPassword | ConvertTo-SecureString -fromplaintext -force}
In this context for example, $_ is not even used; you have to use $this to refer to the parent object.
This does result in the script being processed every time the property is accessed though. If you don't want that, and want to do a static value assignment that's calculated per object, then you must enumerate yourself:
import-csv .\users.csv | ForEach-Object {
$val = $_ | select plaintextPassword | ConvertTo-SecureString -fromplaintext -force
$_ | add-member -passthru -memberType NoteProperty -value $val -Force
}
One solution I've found is to not bother with Add-Member, instead use a calculated property like so:
import-csv .\users.csv |
select -Property *, #{
n="encodedPassword";
e={$_.plaintestPassword | ConvertTo-SecureString -fromplaintext -force}}
(Removed broken add-member after comment from #PetSerAl)

PowerShell function ValueFromPipelineByPropertyName not using alias parameter name

Trying to create a function that takes objects on the pipeline using the alias property. I'm not sure where this is going wrong.
Example of the process:
function Get-Name
{
Param
(
[Parameter(ValueFromPipelineByPropertyName=$true)]
[alias("givenname")]
[System.String] $FirstName,
[Parameter(ValueFromPipelineByPropertyName=$true)]
[alias("sn")]
[System.String] $LastName
)
write-host "firstName = $FirstName / $($FirstName.GetType().FullName)"
Write-host "LastName = $LastName / $($LastName.GetType().FullName)"
}
If I run this command:
Get-Aduser -filter {sn -eq 'smith'} -properties sn,givenname | Get-Name
the output looks like this:
firstName = / string
LastName = / string
The Function never seems to grab the sn and givenname attributes from the passed in object. What am I missing?
The AD Cmdlets are to blame here
The problem here is that the AD Cmdlets return objects in really non-standard ways. For instance, with any other cmdlet if you take the output of the command and select a non-existing property, you'll get back nothing, like this:
get-date | select Hamster
Hamster
-------
>
See, nothing. Sure, it says Hamster, but there is no actual Object there. This is standard PowerShell behavior.
Now, look at what Get-ADUser does instead:
get-aduser -Filter {sn -eq 'adkison'} | select Hamster
Hamster
-------
{}
It creates a $null! So what will happen with your function is that PowerShell will look for a property of -LastName or -FirstName, get a $null and then stop right there. It sucks!
The best way around this is to swap the parameter names like this, and it will still work:
function Get-Name
{
Param
(
[Parameter(ValueFromPipelineByPropertyName=$true)]
[alias('FirstName')]
[System.String] $givenname,
[Parameter(ValueFromPipelineByPropertyName=$true)]
[alias("sn","lastname")]
[System.String] $Surname
)
write-host "firstName = $givenname / $($givenname.GetType().FullName)"
Write-host "LastName = $SurName / $($SurName.GetType().FullName)"
}
get-aduser -Filter {sn -eq 'adkison'} | Get-Name
firstName = James / System.String
LastName = Adkison / System.String
Want to know more?
Check out this awesome answer from /u/JBSmith on the topic.
From what I've been able to determine, it isn't technically the AD cmdlets that are to blame, but the types in the Microsoft.ActiveDirectory.Management namespace--in this case, ADUser. The properties on ADUser are ultimately all just stored in a private SortedDictionary and fetched through get accessors, which might explain why it doesn't work quite as expected.
As alluded to by Colyn1337 in a previous comment, ADUser doesn't contain a property (or key) named either sn or LastName by default, so you'd need to either include an alias of Surname on your LastName parameter or select sn in your Get-ADUser call:
Get-ADUser -Filter {sn -eq 'Adkison'} -Properties sn | Get-Name
That still won't work, but from there you can just pipe to Select-Object before piping to your function:
Get-ADUser -Filter {sn -eq 'Adkison'} -Properties sn | Select * | Get-Name
Of course, you could also just select the specific properties you need instead of * in Select-Object. I assume this works because it resolves the ADUser dictionary into a PSCustomObject with concrete properties. Once resolved, they will match aliases as well as the actual parameter names.

Foreach-Object make mutable copy of $_ in PowerShell

I want to convert entries from Windows Event log to JSON. But I want to preformat some fields. Using ForEach-Object looks like natural decicion for me, but when I try to change attributes there like this:
Get-EventLog System -Newest 2 | % { $_.EntryType = "$($_.EntryType)" } | ConvertTo-Json
it gives me error:
'EntryType' is a ReadOnly property.
How do I made a writable copy of $_ object, or preformat objects before converting to JSON?
You should be able to use Select-Object to do what you want. Select-Object will create entirely new objects (of type PSCustomObject) that you can customize. You can also limit the properties that you actually want, and you can define your own calculated properties.
See this article for more information about calculated properties.
Get-EventLog System -Newest 2 |
Select-Object Index, Time, Source, InstanceID, #{Name='MyEntryType';Expression={$_.EntryType } } |
ConvertTo-Json