The following code snipit works in PowerShell v2, but not v4.. In the release notes for PowerShell v3 is explains that you cannot set the IsFilter property on an unnamed script block. I believe that's exactly what I have, but I don't understand what change to make..
Any help would be appreciated.
function Stop-WindowsService
{
param(
[Parameter(Mandatory=$true,ValueFromPipeline=$true)]
$fromPipe,
[Parameter(ParameterSetName='static',Mandatory=$true,Position=0)]
[ValidateNotNullOrEmpty()]
[string]$name,
[Parameter(ParameterSetName='dynamic',Mandatory=$true,Position=0)]
[ValidateNotNull()]
[ScriptBlock]$scriptReturningName,
[Parameter(Mandatory=$false)]
[ValidateRange(1,86400)]
[int]$timeout = 60
)
Process {
$server = $_
if($PsCmdlet.ParameterSetName -eq 'dynamic') {
$scriptReturningName.IsFilter = $true
$name = ($server | &$scriptReturningName)
}
Write-Verbose "$($server.Name): $name ==> Checking"
$service = $server | Get-WindowsServiceRaw $name
Related
I write own powershell func for debug like:
function StartDebug {
param (
[PARAMETER(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
$FunctionName,
[PARAMETER(Mandatory = $false)]
$OtherArg
)
try {& $FunctionName $OtherArg} catch {...} finally {...}
and use it everyway, but i need more arg after $FunctionName. is it realistic to pass many arguments in this case bec use from 0 to 10 arg. do I have to list all the arguments that can be in the parameters of the function? like:
function StartDebug {
param (
[PARAMETER(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
$FunctionName,
[PARAMETER(Mandatory = $false)]
$OtherArg,
[PARAMETER(Mandatory = $false)]
$OtherArg1,
[PARAMETER(Mandatory = $false)]
$OtherArg2,
[PARAMETER(Mandatory = $false)]
$OtherArg3
)
try {& $FunctionName $OtherArg OtherArg1 OtherArg2 OtherArg3 } catch {...} finally {...}
but i dont use positional parameters in code and too many named parameters in code (~100)
Interested in any ideas about this. tnx!
The magic word is Splatting. You can provide an array or a hashtable containing your arguments to a function. The splatter is written with an #VariableName instead of the $:
function StartDebug {
param (
[PARAMETER(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
$FunctionName,
[PARAMETER(Mandatory = $false)]
$OtherArg
)
try {& $FunctionName #OtherArg # Watch out for the # in the OtherArg
} catch {$_} finally {}
}
$FunctionName = 'Get-ChildItem'
$Splatter = #{
Path = 'C:\'
Filter = 'Users'
Directory = $true
}
$Splatter2 = #('c:\')
StartDebug $FunctionName $Splatter
StartDebug $FunctionName $Splatter2
However if you want to use single items as $OtherArg you will have to provide them as single element array as can be seen with $Splatter2. Or extend your function to transform single arguments in arrays automatically, but thats up to you.
I think you better run it using scriptblock:
$result = Invoke-DeepDebug { Get-ChildItem -Path 'C:\' ; Get-Service -InformationAction Continue}
And in Invoke-DeepDebug you can work with $Command.AST as deep and detailed as you want.
Function Invoke-DeepDebug {
param(
[Parameter(Mandatory=$true, Position=0)]
[Scriptblock]$Command
)
Write-Host -f Cyan "Executing " -n
Write-Host -f Magenta $Command.Ast.Extent.Text -n
Write-Host -f Yellow " ... " -n
$result = $null
try {
$result = Invoke-Command $Command -ErrorAction Stop
Write-Host -f Green "OK!"
} catch {
Write-Host -f Red "Error"
Write-Host -f Red "`t$($_.Exception.Message)"
}
return $result
}
Trying to learn Pester (v5.0.4 and PS v7.0.3). I have these files
Get-Planet2.ps1:
function Get-Planet ([string]$Name = '*') {
$planets = #(
#{ Name = 'Mercury' }
#{ Name = 'Venus' }
#{ Name = 'Earth' }
#{ Name = 'Mars' }
#{ Name = 'Jupiter' }
#{ Name = 'Saturn' }
#{ Name = 'Uranus' }
#{ Name = 'Neptune' }
) | ForEach-Object { [PSCustomObject] $_ }
$planets | Where-Object { $_.Name -like $Name }
}
Get-Planet2.Tests.ps1:
BeforeAll {
# This will bring the function from the main file back to scope.
. $PSScriptRoot/Get-Planet2.ps1
param(
[parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$name,
[parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$title,
[parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$InputFile
)
}
Describe 'Get-Planet' {
It 'Given no parameters, it lists all 8 planets' {
$allPlanets = Get-Planet
$allPlanets.Count | Should -Be 8
}
It 'Earth is the third planet in our Solar System' {
$allPlanets = Get-Planet
$allPlanets[2].Name | Should -Be 'Earth'
}
It 'Pluto is not part of our Solar System' {
$allPlanets = Get-Planet
$plutos = $allPlanets | Where-Object Name -EQ 'Pluto'
$plutos.Count | Should -Be 0
}
It 'Planets have this order: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune' {
$allPlanets = Get-Planet
$planetsInOrder = $allPlanets.Name -join ', '
$planetsInOrder | Should -Be 'Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune'
}
}
Call-Get-Planet2.ps1:
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$path = "$here/Get-Planet2.Tests.ps1"
$parameters = #{name = "John"; title = "Sales"; InputFile = "$here/test.json"}
write-host "Path: $path"
write-host $parameters
Invoke-Pester -Script #{Path=$path;Parameters=$parameters}
My Problem:
When I run Call-Get-Planet2.ps1, I ended with the below error. I can't figure out why it is doing this. Need guidance. Thanks
System.Management.Automation.RuntimeException: No test files were found and no scriptblocks were provided.
at Invoke-Pester, /Users/myaccount/.local/share/powershell/Modules/Pester/5.0.4/Pester.psm1: line 4520
at , /Users/myaccount/repos/myrepo/Pester/Call-Get-Planet2.ps1: line 8
at , : line 1
If you encounter this error ('No test files were found and no scriptblocks were provided'), you should follow LievenKeersmaekers' suggestion (made in the question comments), which is to execute the test file more directly by doing something like the following:
cd {TO\A\FOLDER\WITH\A\TEST_FILE}
Invoke-Pester -Path .\{TEST_FILE}.ps1
You'll likely see a more descriptive error message that will help you to solve your issue.
Pester v5 had a breaking change and removed support for Invoke-Pester -Script <hashtable>. Pester 5.1 re-introduced support for script parameters using containers which you create with New-PesterContainer -Path <file> -Data <parametersHashtable> and invoke using Invoke-Pester -Container ....
For the original demo, upgrade to Pester 5.1 or later and modify:
Get-Planet2.Tests.ps1 to:
# Script parameters need to be in the root of the file.
param(
[parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$name,
[parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$title,
[parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$InputFile
)
BeforeAll {
# This will bring the function from the main file back to scope.
. $PSScriptRoot/Get-Planet2.ps1
}
Describe 'Get-Planet' {
It 'Given no parameters, it lists all 8 planets' {
$allPlanets = Get-Planet
$allPlanets.Count | Should -Be 8
}
It 'Earth is the third planet in our Solar System' {
$allPlanets = Get-Planet
$allPlanets[2].Name | Should -Be 'Earth'
}
It 'Pluto is not part of our Solar System' {
$allPlanets = Get-Planet
$plutos = $allPlanets | Where-Object Name -EQ 'Pluto'
$plutos.Count | Should -Be 0
}
It 'Planets have this order: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune' {
$allPlanets = Get-Planet
$planetsInOrder = $allPlanets.Name -join ', '
$planetsInOrder | Should -Be 'Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune'
}
}
Call-Get-Planet2.ps1 to:
# $PSScriptRoot is easier than Split-Path :-)
$here = $PSScriptRoot
$path = "$here/Get-Planet2.Tests.ps1"
$parameters = #{name = 'John'; title = 'Sales'; InputFile = "$here/test.json" }
Write-Host "Path: $path"
Write-Host $parameters
# -Script <hashtable> is not supported in v5.
# Use New-PesterContainer
$container = New-PesterContainer -Path $path -Data $parameters
Invoke-Pester -Container $container
I've written the following function in test.ps1 and I would like to make a choise when running thsi script to start/stop/.. :
function getState($SeviceName) {
$server = #('host_1', 'host_2')
# get status
$server | % {Write-Host "verify: $_"; Get-Service -ComputerName $_ -Name SeviceName
}
I would like to provide $ServiceName as argument (with stdin) how can I do it? => somthing like choose 1 to start 2 to stop ...
To use switch/case in Powershell
$doAction = {"Stop-Service", "Start-service"}
$server | % {Write-Host "verify: $_"; Get-Service -ComputerName $_ -Name SeviceName | $doAction}
How do I use the switch to select start or stop?
Here's a function that will do what you're asking for:
function Get-State {
[CmdletBinding()]
[OutputType('System.ServiceProcess.ServiceController')]
param(
[Parameter(Position = 0, Mandatory)]
[ValidateSet('Start', 'Stop', 'Get')]
[string] $Action,
[Parameter(Position = 1, ValueFromPipeline, Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $ServiceName
)
begin {
$serverList = #('host_1', 'host_2')
}
process {
foreach ($server in $serverList) {
try {
$svc = Get-Service -ComputerName $server -Name $ServiceName -ErrorAction Stop
} catch {
throw "Failed to find service $ServiceName on $server! $PSItem"
}
switch ($Action) {
'Start' { $svc | Start-Service -PassThru }
'Stop' { $svc | Stop-Service -Force -PassThru }
default { $svc }
}
}
}
}
It utilizes advanced function features and attributes to take pipeline input (stdin in your words). I'd suggest reading this documentation.
You can add argument to a script by adding parameters to it.
On the top of your script file put:
Param
(
[parameter()]
[String[]]$YourArgumentVariable
[parameter()]
[switch] $MySwitch
)
With a function it goes right after the function definition. So in your case:
function getState($SeviceName) {
Param
(
[parameter()]
[String[]]$server
[parameter()]
[switch] $MySwitch
)
# get status
$server | % {Write-Host "verify: $_"; Get-Service -ComputerName $_ -Name SeviceName
}
A switch basically sets a boolean to true or false.
So in this if you call the script with -MySwitch it will set the variable $MySwitch to true. Else it will remain false.
Don Jones has written a good getting started article on paramters that I would recommend you checking out.
Do note that there are loads of things you can define in the paramter. Like if you want to make sure it is always filled you can set
[parameter(Mandatory=$true)]
This is just one of many examples of what you can do with paramters.
$RunspaceCollection = #()
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(1,5)
$RunspacePool.Open()
$code = #({'somecode'},{'someothercode'})
Foreach ($test in $case) {
$finalcode= {
Invoke-Command -ScriptBlock [scriptblock]::create($code[$test])
}.GetNewClosure()
$Powershell = [PowerShell]::Create().AddScript($finalcode)
$Powershell.RunspacePool = $RunspacePool
[Collections.Arraylist]$RunspaceCollection += New-Object -TypeName PSObject -Property #{
Runspace = $PowerShell.BeginInvoke()
PowerShell = $PowerShell
}}
The finalcode variable doesn't expand when the GetNewClosure() happens, so $code[$test] gets into the runspace instead of actual code and I can't get my desired results. Any advice?
Using the method from the answer I'm getting this in the runspace, but it doesn't execute properly. I can confirm that my command is loaded into runspace (at least while in debugger inside runspace I can execute it without dot sourcing)
[System.Management.Automation.PSSerializer]::Deserialize('<ObjsVersion="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
<SBK> My-Command -Par1 "egweg" -Par2 "qwrqwr" -Par3 "wegweg"</SBK>
</Objs>')
This is what I see in debugger in runspace
Stopped at: $a = Invoke-Command -ScriptBlock { $([System.Management.Automation.PSSerializer]::Deserialize('<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
[DBG]: [Process:8064]: [Runspace12]: PS C:\git\infrastructure_samples>>
Stopped at: $a = Invoke-Command -ScriptBlock { $([System.Management.Automation.PSSerializer]::Deserialize('<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
[DBG]: [Process:8064]: [Runspace12]: PS C:\git\infrastructure_samples>> s
Stopped at: </Objs>')) }
The problem with your code is that AddScript method of PowerShell class is expecting a string, not ScriptBlock. And any closure will be lost when you convert ScriptBlock to string. To solve this, you can pass argument to script with AddArgument method:
$RunspaceCollection = New-Object System.Collections.Generic.List[Object]
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(1,5)
$RunspacePool.Open()
$code = #({'somecode'},{'someothercode'})
$finalcode= {
param($Argument)
Invoke-Command -ScriptBlock ([scriptblock]::create($Argument))
}
Foreach ($test in $case) {
$Powershell = [PowerShell]::Create().AddScript($finalcode).AddArgument($code[$test])
$Powershell.RunspacePool = $RunspacePool
$RunspaceCollection.Add((New-Object -TypeName PSObject -Property #{
Runspace = $PowerShell.BeginInvoke()
PowerShell = $PowerShell
}))
}
I'm not sure if there's a better way off the top of my head, but you can replace the variables yourself with serialized versions of the same.
You can't use $Using: in this case, but I wrote a function that replaces all $Using: variables manually.
My use case was with DSC, but it would work in this case as well. It allows you to still write your script blocks as scriptblocks (not as strings), and supports variables with complex types.
Here's the code from my blog (also available as a GitHub gist):
function Replace-Using {
[CmdletBinding(DefaultParameterSetName = 'AsString')]
[OutputType([String], ParameterSetName = 'AsString')]
[OutputType([ScriptBlock], ParameterSetName = 'AsScriptBlock')]
param(
[Parameter(
Mandatory,
ValueFromPipeline
)]
[String]
$Code ,
[Parameter(
Mandatory,
ParameterSetName = 'AsScriptBlock'
)]
[Switch]
$AsScriptBlock
)
Process {
$cb = {
$m = $args[0]
$ser = [System.Management.Automation.PSSerializer]::Serialize((Get-Variable -Name $m.Groups['var'] -ValueOnly))
"`$([System.Management.Automation.PSSerializer]::Deserialize('{0}'))" -f $ser
}
$newCode = [RegEx]::Replace($code, '\$Using:(?<var>\w+)', $cb, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
if ($AsScriptBlock.IsPresent) {
[ScriptBlock]::Create($newCode)
} else {
$newCode
}
}
}
A better way for me to do this replacement would probably be to use the AST instead of string replacement, but.. effort.
Your Use Case
$finalcode= {
Invoke-Command -ScriptBlock [scriptblock]::create($Using:code[$Using:test])
} | Replace-Using
For better results you might assign a variable first and then just insert that:
$value = [scriptblock]::Create($code[$test])
$finalcode= {
Invoke-Command -ScriptBlock $Using:value
} | Replace-Using
I am trying to collect user profile information for users on a machine and I was wondering if I could get it with gwmi. Here is how I get printers for the current user:Get-WmiObject win32_printer. How can I get the same info for the user "Test" on the same machine?
As it happens, I can't sleep, so I came up with these 2 functions:
function Get-UserSid {
[CmdletBinding()]
param(
[Parameter(
ParameterSetName='NTAccount',
Mandatory=$true,
ValueFromPipeline=$true,
Position=0
)]
[System.Security.Principal.NTAccount]
$Identity ,
[Parameter(
ParameterSetName='DomainAndUser',
Mandatory=$true
)]
[ValidateNotNullOrEmpty()]
[ValidatePattern('^[^\\]+$')]
[String]
$Domain ,
[Parameter(
ParameterSetName='DomainAndUser',
Mandatory=$true
)]
[ValidateNotNullOrEmpty()]
[ValidatePattern('^[^\\]+$')]
[String]
$User
)
Begin {
if ($PSCmdlet.ParameterSetName -eq 'DomainAndUser') {
$Identity = New-Object System.Security.Principal.NTAccount -ArgumentList $Domain,$User
}
}
Process {
$Identity.Translate([System.Security.Principal.SecurityIdentifier])
}
}
function Get-PrinterNameByUser {
[CmdletBinding(DefaultParameterSetName='Ambiguous')]
param(
[Parameter(
ParameterSetName='ByAccount',
Mandatory=$true
)]
[System.Security.Principal.NTAccount]
$Account ,
[Parameter(
ParameterSetName='BySID',
Mandatory=$true
)]
[System.Security.Principal.SecurityIdentifier]
$SID ,
[Parameter(
ParameterSetName='Ambiguous',
Mandatory=$true,
Position=0,
ValueFromPipeline=$true
)]
[ValidateNotNullOrEmpty()]
[String]
$Identity
)
Begin {
Write-Verbose "Parameter Set Name: $($PSCmdlet.ParameterSetName)"
if ($PSCmdlet.ParameterSetName -eq 'ByAccount') {
$SID = $Account | Get-UserSid
}
}
Process {
if ($PSCmdlet.ParameterSetName -eq 'Ambiguous') {
try {
$SID = [System.Security.Principal.SecurityIdentifier]$Identity
} catch [System.InvalidCastException] {
$Account = [System.Security.Principal.NTAccount]$Identity
$SID = $Account | Get-UserSid
}
}
Get-ChildItem -Path "Registry::\HKEY_Users\$($SID.Value)\Printers" | Select-Object -ExpandProperty Property -Unique
}
}
Usage
Get-PrinterNameByUser Test
Get-PrinterNameByUser 'domain\test'
Get-PrinterNameByUser 'S-1-S-21-65454546-516413534-4444'
All of those could be piped as well:
'Test' | Get-PrinterNameByUser
'domain\test' | Get-PrinterNameByUser
'S-1-S-21-65454546-516413534-4444' | Get-PrinterNameByUser
'S-1-S-21-65454546-516413534-4444','user1','machine\user2','domain\user3' | Get-PrinterNameByUser
Explanation
In the registry at HKU\S-ID-HERE\Printers there are some keys with properties. The property names are the printers. I wasn't able to test this on enough machines, so I wasn't certain which key(s) I should check, and whether they would be different depending on whether it was a local or network printer, etc., so I'm just getting the properties from all the keys and returning the unique ones.
The helper function Get-UserSid just provides a convenient way to get a SID from a user name.
Most of Get-PrinterNameByUser is just code to figure out what you've given it and translate it at needed. The meat of it that returns what you want is just the one line:
Get-ChildItem -Path "Registry::\HKEY_Users\$($SID.Value)\Printers" | Select-Object -ExpandProperty Property -Unique