does ArgumentTransformationAttribute return Objects out of the Transform Method? - powershell

this is a class to transform a computername into a CimSession type:
using namespace System.Management.Automation ;
using namespace System.Net.NetworkInformation ;
using namespace Microsoft.Management.Infrastructure ;
enum Protocol {
WsMan
Dcom
}
class ComputernameTransformationAttribute : ArgumentTransformationAttribute {
<# ValidateComputername #>
hidden [CimSession] NewCimsession([string] $inputData){
$Protocol = [Protocol]::WsMan -as [string] ;
$CimSessionOption = New-CimSessionOption -Protocol $Protocol ;
# [CimSession] $Cimsession = New-CimSession #Param
[CimSession] $Cimsession = [CimSession]::Create($inputData, $CimSessionOption);
return $CimSession;
}
# Transform() method is called whenever there is a variable or parameter assignment.
[System.Object] Transform(
[EngineIntrinsics]$engineIntrinsics,
[System.Object]$inputData
){
return $($this.NewCimsession($inputData));
}
}
i tried to test hier but the Transform method is not returning any object, just a splatted info i can't use.
am i doing something wrong hier ?
hier ist my test funtion:
function Get-ADComputerDescription {
[CmdletBinding()]
#[OutputType([GKMAdinfo])]
param (
# ComputerName
[Parameter(ValueFromPipeline)]
# [ValidateNotNullOrEmpty()]
[Alias("CN","Machine","Name")]
[ComputernameTransformation()]
[string]$ComputerName
)
$ComputerName
Get-CimInstance Win32_ComputerSystem -CimSession $ComputerName
}
thanks for your help

Related

Get value from pipeline with ValueFromPipelineByPropertyName

I have some issues getting a value from the pipeline using ValueFromPipelineByPropertyName.
When I run Get-Input -ComputerName 'PC-01' | Get-Data the cmdlet Get-Input should simply return the computer name "PC-01", whereas the Get-Data function should return "Value passed from Get-Input: PC-01". Instead, I get this error:
Get-Data : The input object cannot be bound to any parameters for the command
either because the command does not take pipeline input or the input and its
properties do not match any of the parameters that take pipeline input.
At line:1 char:33
+ Get-Input -ComputerName PC-01 | Get-Data
+ ~~~~~~~~
+ CategoryInfo : InvalidArgument: (PC-01:PSObject) [Get-Data], ParameterBindingException
+ FullyQualifiedErrorId : InputObjectNotBound,Get-Data
I have build these two small sample cmdlets just to get the hang of working with the pipeline.
function Get-Input {
[CmdletBinding()]
Param(
[Parameter(
Mandatory = $true,
ValueFromPipelineByPropertyName = $true
)]
[string]$ComputerName
)
Process {
Write-Output -InputObject $ComputerName
}
}
function Get-Data {
[CmdletBinding()]
Param(
[Parameter(
Mandatory = $true,
ValueFromPipelineByPropertyName = $true
)]
[string]$ComputerName
)
Process {
Write-Output -InputObject "Value passed from Get-Input: $($ComputerName)."
}
}
If I change $ComputerName to $Name and run the following, it works:
PS C:\Users\frede> Get-Service -Name AdobeARMservice | Get-Data
Value passed from Get-Input: AdobeARMservice.
If I have grasped the concept of the pipeline in PowerShell, I should be able to run the following command Get-Input -ComputerName 'PC-01' | Get-Data and have the ComputerName passed to Get-Data.
Is there something I need to declare somewhere?
As the name (ValueFromPipelineByPropertyName) indicates, you're telling the parser to bind a value based on a property name.
The Get-Input function will need to output an object that has a property named ComputerName for this to work:
function Get-Input
{
[CmdletBinding()]
param
(
[Parameter(
Mandatory = $true,
ValueFromPipelineByPropertyName = $true
)]
[string]$ComputerName
)
process
{
Write-Output $(New-Object psobject -Propert #{ComputerName = $ComputerName})
}
}
Now you can do:
Get-Input -ComputerName 'PC-01' |Get-Data
If you want Get-Data to support computer name input from Get-Service, you'll have to add an alias that matches the appropriate property name on the object types output by Get-Service, ie. MachineName:
function Get-Data
{
[CmdletBinding()]
param
(
[Parameter(
Mandatory = $true,
ValueFromPipelineByPropertyName = $true
)]
[Alias('MachineName')]
[string]$ComputerName
)
process
{
Write-Output -InputObject "Value passed from Get-Input: $($ComputerName)."
}
}
And now both of these will work:
Get-Service -Name AdobeARMService |Get-Data
Get-Input -ComputerName PC-01 |Get-Data
You will need this bud.
ValueFromPipelineByPropertyName is for boolean ($True / $False) and isn't looking for your string.
[CmdletBinding()]
param
(
[Parameter(
Mandatory = $true,
ValueFromPipeline = $true
)]
[string]$ComputerName
)
you have to write also ValueFromPipeline=$true:
Mandatory = $true,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName = $true
Greetings
Jannik

Powershell to start/stop with arg

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.

ValueFromPipelineByPropertyName can I shift function next positional parameters?

Considering to functions designed to use values by property name, the second function have his first argument passed by pipeline. Can I use positional parameter for this second function?
Example:
function Get-Customer {
[CmdletBinding()]
Param(
[string]$CustomerName = "*"
)
Process {
# real process iterate on fodlers.
New-Object PSObject -Property #{
CustomerName = $CustomerName;
}
}
}
function Get-Device {
[CmdletBinding()]
Param(
[Parameter(ValueFromPipelineByPropertyName)]
[string]$CustomerName = "*",
[string]$DeviceName = "*"
)
Process {
# real process iterate on fodlers.
New-Object PSObject -Property #{
CustomerName=$CustomerName;
DeviceName=$DeviceName
}
}
}
You can use it like:
Get-Customer "John" | Get-Device
Get-Customer "John" | Get-Device -DeviceName "Device 1"
But can you do this (actually with provided code it doesn't work)?
Get-Customer "John" | Get-Device "Device 1"
You need to define $DeviceName as the first positional parameter for that to work:
function Get-Device {
[CmdletBinding()]
Param(
[Parameter(ValueFromPipelineByPropertyName)]
[string]$CustomerName = "*",
[Parameter(Position=0)]
[string]$DeviceName = "*"
)
Process {
# real process iterate on fodlers.
New-Object PSObject -Property #{
CustomerName = $CustomerName;
DeviceName = $DeviceName
}
}
}

Expand variable with scriptblock inside variable in a loop with runspaces

$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

Powershell function failure

I swear I'm missing something easy here...
Here's a simple script to get disk info:
function get-disks {
try { $disks = gwmi win32_logicaldisk -co $server}
catch { write "$server : Can't connect"}
}
get-disks
$disk.deviceid
The gwmi command alone works perfectly. The "$disks = gwmi..." command alone works perfectly. The try {...}catch{...} lines run alone work perfectly.
But as soon as I load the function and call 'get-disks' I receive no errors, but $disks is empty.
The $server parameter, and the $disks variable are local to the inside function and not visible (not defined) outside of the function.
You need to provide the server name as a function parameter (from the outside in), and you need to return the $disks variable value from the function (from the inside out) and capture it's value.
function Get-Disks {
param(
[Parameter(Mandatory = $true)]
[string] $Server
)
try {
$result = gwmi win32_logicaldisk -co $Server;
return $result # <--
}
catch { write "$Server : Can't connect"}
}
$disks = Get-Disks -Server "localhost"
Note that the $result variable inside the function is another
variable the the $disks variable outside of the function.
For simplicity, you can write the function as follows:
function Get-Disks {
param(
[Parameter(Mandatory = $true)]
[string] $Server
)
# just return the output of gwmi directly
gwmi win32_logicaldisk -co $Server;
}