I want to add a custom method to an existing object. My problem is I may not find out how to make it accept parameters.
In this greatly simplified example I want to add a script block to a System.IO.FileInfo-Object to output a specific parameter to the screen:
$NewMethodScript = {
param(
[String] $Param1
)
write-host $this.$Param1
#Do lots of more stuff, call functions, etc...
}
$FInfo = [System.IO.FileInfo]::new("C:\File.txt")
$FInfo | Add-Member -MemberType ScriptMethod -Name NewMethod -Value $NewMethodScript
$FInfo.NewMethod "DirectoryName"
Related
I am trying to specify the value of the third parameter of the method, while still letting the second parameter in the method default.
I was able to piece this together to get it working, but I was hoping someone else had a better solution
$o=[PSCustomObject]#{};
Add-Member -MemberType ScriptMethod -InputObject $o -Name 'WrapText' -Value {
param($S,$Open='"',$Close)
if($Close){
"$Open$S$Close"
}else{
"$Open$S$Open"
}
}
$DefaultValues = #{};
$o.WrapText.Script.Ast.ParamBlock.Parameters | %{
$DefaultValues.($_.Name.ToString()) = $_.DefaultValue.Value
}
$o.WrapText('Some Text',$DefaultValues.'$Open','|')
In order to check whether an argument was bound to a parameter, you'll want to use $PSBoundParameters:
Add-Member -MemberType ScriptMethod -InputObject $o -Name 'WrapText' -Value {
param($S,$Open='"',$Close='"')
if($PSBoundParameters.ContainsKey('Close')){
"$Open$S$Close"
}else{
"$Open$S$Open"
}
}
Now the if condition is only $true if a third argument is supplied:
PS ~> $o.WrapText('abc')
"abc"
PS ~> $o.WrapText('abc',"'")
'abc'
PS ~> $o.WrapText('abc',"'",'$')
'abc$
There's something that I can't quite seem to wrap my head around when trying to do object references in Powershell. Not sure if there's something that I am missing out on.
A sample code illustrating this problem is as follows:
function Create-Custom-Object {
$oResult = New-Object -TypeName PSObject -Property (#{
"Test" = $(Get-Date);
})
Add-Member -memberType ScriptMethod -InputObject $oResult -Name "GetTest" -Value {
return $this.Test;
}
return $oResult
}
function Create-Wrapper-Object {
$oObject = $(Create-Custom-Object)
$oResult = New-Object -TypeName PSObject -Property (#{
"Object" = $oObject;
"Test" = $(Get-Date);
})
Add-Member -MemberType ScriptMethod -InputObject $oResult -Name "WrapTest" -Value {
return $this.Object.GetTest()
}
return $oResult
}
$oCustom = Create-Custom-Object
sleep 5
$oWrapper = Create-Wrapper-Object
echo "Custom-Test: $($oCustom.Test)"
echo "Wrapper-Test: $($oWrapper.Test)"
echo "GetTest: $($oCustom.GetTest())"
echo "WrapTest: $($oWrapper.WrapTest())"
When run, the output is as per below:
>powershell -file test.ps1
Custom-Test: 11/20/2017 16:10:19
Wrapper-Test: 11/20/2017 16:10:24
GetTest: 11/20/2017 16:10:19
WrapTest: 11/20/2017 16:10:24
What puzzled me is that the call to WrapTest() on the wrapper object returns the "Test" attribute value from the wrapper object instead of the embedded custom object. Why is Powershell behaving like this?
I suspect that the problem here (based on the assumed intent of the sleep 5) is that $oCustom is assigned a Custom-Object and then 5 seconds later $oWrapper is assigned a Wrapper-Object which contains a new Custom-Object with essentially the same [DateTime] value (to the nearest second), not the (intended?) previously created $oCustom. WrapTest() is not returning the Test member of $oWrapper but the indistinguishable Test member of its own Custom-Object in $oWrapper.Object. In order to create a (generic) wrapper object, you need something to wrap, otherwise it's really just a (specific) nested object. Something like this:
function Create-Wrapper-Object {
param ($ObjectToWrap)
$oResult = New-Object -TypeName PSObject -Property (#{
"Object" = $ObjectToWrap; # presumably with a GetTest() method
"Test" = $(Get-Date); # remember the time of wrapping
})
Add-Member -MemberType ScriptMethod -InputObject $oResult -Name "WrapTest" -Value {
return $this.Object.GetTest()
}
return $oResult
}
With the (assumed to be) desired result:
$oCustom = Create-Custom-Object
sleep 5
$oWrapper = Create-Wrapper-Object $oCustom
echo "Custom-Test: $($oCustom.Test)"
Custom-Test: 05/31/2021 08:52:30
echo "Wrapper-Test: $($oWrapper.Test)"
Wrapper-Test: 05/31/2021 08:52:35
echo "GetTest: $($oCustom.GetTest())"
GetTest: 05/31/2021 08:52:30
echo "WrapTest: $($oWrapper.WrapTest())"
WrapTest: 05/31/2021 08:52:30
How can I implement a get/set property with PowerShell class?
Please have a look on my example below:
Class TestObject
{
[DateTime]$StartTimestamp = (Get-Date)
[DateTime]$EndTimestamp = (Get-Date).AddHours(2)
[TimeSpan] $TotalDuration {
get {
return ($this.EndTimestamp - $this.StartTimestamp)
}
}
hidden [string] $_name = 'Andreas'
[string] $Name {
get {
return $this._name
}
set {
$this._name = $value
}
}
}
New-Object TestObject
You can use Add-Member ScriptProperty to achieve a kind of getter and setter:
class c {
hidden $_p = $($this | Add-Member ScriptProperty 'p' `
{
# get
"getter $($this._p)"
}`
{
# set
param ( $arg )
$this._p = "setter $arg"
}
)
}
Newing it up invokes the initializer for $_p which adds scriptproperty p:
PS C:\> $c = [c]::new()
And using property p yields the following:
PS C:\>$c.p = 'arg value'
PS C:\>$c.p
getter setter arg value
This technique has some pitfalls which are mostly related to how verbose and error-prone the Add-Member line is. To avoid those pitfalls, I implemented Accessor which you can find here.
Using Accessor instead of Add-Member does an amount of error-checking and simplifies the original class implementation to this:
class c {
hidden $_p = $(Accessor $this {
get {
"getter $($this._p)"
}
set {
param ( $arg )
$this._p = "setter $arg"
}
})
}
Here's how I went about it
[string]$BaseCodeSignUrl; # Getter defined in __class_init__. Declaration allows intellisense to pick up property
[string]$PostJobUrl; # Getter defined in __class_init__. Declaration allows intellisense to pick up property
[hashtable]$Headers; # Getter defined in __class_init__. Declaration allows intellisense to pick up property
[string]$ReqJobProgressUrl; # Getter defined in __class_init__. Declaration allows intellisense to pick up property
# Powershell lacks a way to add get/set properties. This is a workaround
hidden $__class_init__ = $(Invoke-Command -InputObject $this -NoNewScope -ScriptBlock {
$this | Add-Member -MemberType ScriptProperty -Name 'BaseCodeSignUrl' -Force -Value {
if ($this.Production) { [CodeSign]::CodeSignAPIUrl } else { [CodeSign]::CodeSignTestAPIUrl }
}
$this | Add-Member -MemberType ScriptProperty -Name 'PostJobUrl' -Force -Value {
"$($this.BaseCodeSignUrl)/Post?v=$([CodeSign]::ServiceApiVersion)"
}
$this | Add-Member -MemberType ScriptProperty -Name 'Headers' -Force -Value {
#{
_ExpireInMinutes=[CodeSign]::Timeout.Minutes;
_CodeSigningKey=$this.Key;
_JobId=$this.JobId;
_Debug=$this.Dbg;
_Token=$this.Token;
}
}
$this | Add-Member -MemberType ScriptProperty -Name 'ReqJobProgressUrl' -Force -Value {
"$($this.BaseCodeSignUrl)Get?jobId=$($this.JobId)"
}
});
We've written a powershell script which processes images from an internal system and sends them off to another system. Now another part of the business would like to hook into this and do their own processing of the indata and push it to yet another system. Asking around, there are several intrested parties around the company, so I'd like to make it simple to add these new systems.
A first prototype simple opens all .ps1 files in a folder and runs a specially named function in there and hopes for the best, basically. However, this seems like it could be improved. Is there some established powershell best practice to do some plugin-like system? If not, given that this executes in a quite secured environment, and new modules will be checked in by administrators, are there any problems with my above approach?
Why wouldn't you use a config file for your main script, explicitly telling what script and what function to call? Something like this (warning: this is copy/pasted and adapted code from something I wrote. Might contain a few glitches, but this gives you the general idea):
<?xml version="1.0"?>
<configuration>
<Plugins>
<Plugin Path="c:\blah\plugin1.ps1" PowerShellFunction="My-Plugin-Function" />
</Plugins>
</configuration>
In your main script:
function Load-Plugins
{
param (
[parameter(Mandatory = $true)][xml] $config,
[parameter(Mandatory = $true)][string] $nodeType
)
$plugins = #{}
foreach ($pluginNode in $config.SelectNodes($nodeType))
{
if ($pluginNode)
{
$Path = $pluginNode.Path
$powerShellFunction = $pluginNode.PowerShellFunction
$plugin = New-Object Object |
Add-Member -MemberType NoteProperty -Name "Path" -Value $Path -PassThru |
Add-Member -MemberType NoteProperty -Name "PowerShellFunction" -Value $powerShellFunction -PassThru
$plugins[$Path] = $plugin
}
}
return $plugins
}
function Execute-Plugins
{
param (
[parameter(Mandatory = $true)][hashtable] $plugins
)
$Error.Clear()
if (!$plugins.Values)
{ return }
foreach ($plugin in $plugins.Values)
{
& .\$plugin.Path
Invoke-Expression "$($plugin.PowerShellFunction)"
}
}
function Load-Script-Config
{
param (
[parameter(Mandatory = $false)][string] $configFile
)
if (!$configFile)
{ $configFile = (Get-PSCallStack)[1].Location.Split(':')[0].Replace(".ps1", ".config") }
return [xml](Get-Content $configFile)
}
$pluginConfig = Load-Script-Config
$plugins = Load-Plugins $config "configuration/Plugins/Plugin"
Execute-Plugins $plugins
Here is the code:
I get projects list
Iterate through the list and add member on the current value
doesn't work
[psobject] $tfs = get-tfs "http://myserver:8080/tfs/defaultcollection"
[psobject] $projects = $tfs.CSS.ListAllProjects()
foreach($pro in $projects){
$pro | add-member -membertype noteproperty -name TFS -value $tfs
$test = $pro.TFS
}
Since you already have the $tfs object, why not just pass it with the $projects collection to whatever code you're calling after this? That way you're only dealing with it once, rather than looping over all the projects and adding the same variable to each. So something like this:
[psobject] $tfs = get-tfs "http://myserver:8080/tfs/defaultcollection"
[psobject] $projects = $tfs.CSS.ListAllProjects()
myFunction $projects $tfs