I have the following method declared in a module I've called Common.psm1:
function Test-Any ([ScriptBlock]$FilterScript = $null)
{
begin {
$done = $false
}
process {
if (!$done)
{
if (!$FilterScript -or ($FilterScript | Invoke-Expression)){
$done = $true
}
}
}
end {
$done
}
}
Set-Alias any Test-Any -Scope Global
Now in another module, I have the following validation:
$id = 1
if($notifications | any { $_.Id -eq $id })
{
# do stuff here
return
}
I receive the following error:
Invoke-Expression : The variable '$id' cannot be retrieved because it has not been set.
The interesting thing is that if I move the Test-Any definition to the calling module, it works like a charm.
How can I make this work without copying Test-Any to my other modules and without changing this syntax:
if($notifications | any { $_.Id -eq $id })
EDIT 1:
There seems to be some debate about whether or not my code should work. Feel free to try this on your own machine:
function Test-Any ([ScriptBlock]$FilterScript = $null)
{
begin {
$done = $false
}
process {
if (!$done)
{
if (!$FilterScript -or ($FilterScript | Invoke-Expression)){
$done = $true
}
}
}
end {
$done
}
}
Set-Alias any Test-Any -Scope Global
$id = 3
$myArray = #(
#{Id = 1},
#{Id = 2},
#{Id = 3},
#{Id = 4},
#{Id = 5},
#{Id = 6},
#{Id = 7},
#{Id = 8}
)
$myEmptyArray = #()
$myArray | any #returns true
$myArray | any {$_.Id -eq $id} #returns true
$myEmptyArray | any #returns false
$myEmptyArray | any {$_.Id -eq $id} #returns false
EDIT 2:
I just discovered that you only encounter this issue, when Test-Any resides in one loaded module and the calling code resides in a second module using Set-StrictMode -Version Latest. If you turn off StrictMode, you don't get the error, but it also doesn't work.
EDIT 3:
Needless to say this works perfectly fine:
$sb = [Scriptblock]::Create("{ `$_.Id -eq $id }")
if($notifications | any $sb)
But seriously takes away from the simplicity and intuitiveness I am trying to obtain
Invoke-Expression (which, when possible, should be avoided) implicitly recreates the script block passed from the caller's scope, via its string representation, in the context of the module, which invalidates any references to the caller's state in the script-block code (because modules generally don't see an outside caller's state, except for the global scope).
The solution is to execute the script block as-is, but provide it pipeline input as passed to the module function:
# Note: New-Module creates a *dynamic* (in-memory only) module,
# but the behavior applies equally to regular, persisted modules.
$null = New-Module {
function Test-Any ([ScriptBlock] $FilterScript)
{
begin {
$done = $false
}
process {
if (!$done)
{
# Note the use of $_ | ... to provide pipeline input
# and the use of ForEach-Object to evaluate the script block.
if (!$FilterScript -or ($_ | ForEach-Object $FilterScript)) {
$done = $true
}
}
}
end {
$done
}
}
}
# Sample call. Should yield $true
$id = 1
#{ Id = 2 }, #{ Id = 1 } | Test-Any { $_.Id -eq $id }
Note: The Test-Any function in this answer uses a similar approach, but tries to optimize processing by stopping further pipeline processing - which, however, comes at the expense of incurring an on-demand compilation penalty the first time the function is called in the session, because - as of PowerShell 7.2 - you cannot (directly) stop a pipeline on demand from user code - see GitHub issue #3821.
Related
I'm basically building my own parallel foreach pipeline function, using runspaces.
My problem is: I call my function like this:
somePipeline | MyNewForeachFunction { scriptBlockHere } | pipelineGoesOn...
How can I pass the $_ parameter correctly into the ScriptBlock? It works when the ScriptBlock contains as first line
param($_)
But as you might have noticed, the powershell built-in ForEach-Object and Where-Object do not need such a parameter declaration in every ScriptBlock that is passed to them.
Thanks for your answers in advance
fjf2002
EDIT:
The goal is: I want comfort for the users of function MyNewForeachFunction - they shoudln't need to write a line param($_) in their script blocks.
Inside MyNewForeachFunction, The ScriptBlock is currently called via
$PSInstance = [powershell]::Create().AddScript($ScriptBlock).AddParameter('_', $_)
$PSInstance.BeginInvoke()
EDIT2:
The point is, how does for example the implementation of the built-in function ForEach-Object achieve that $_ need't be declared as a parameter in its ScriptBlock parameter, and can I use that functionality, too?
(If the answer is, ForEach-Object is a built-in function and uses some magic I can't use, then this would disqualify the language PowerShell as a whole in my opinion)
EDIT3:
Thanks to mklement0, I could finally build my general foreach loop. Here's the code:
function ForEachParallel {
[CmdletBinding()]
Param(
[Parameter(Mandatory)] [ScriptBlock] $ScriptBlock,
[Parameter(Mandatory=$false)] [int] $PoolSize = 20,
[Parameter(ValueFromPipeline)] $PipelineObject
)
Begin {
$RunspacePool = [runspacefactory]::CreateRunspacePool(1, $poolSize)
$RunspacePool.Open()
$Runspaces = #()
}
Process {
$PSInstance = [powershell]::Create().
AddCommand('Set-Variable').AddParameter('Name', '_').AddParameter('Value', $PipelineObject).
AddCommand('Set-Variable').AddParameter('Name', 'ErrorActionPreference').AddParameter('Value', 'Stop').
AddScript($ScriptBlock)
$PSInstance.RunspacePool = $RunspacePool
$Runspaces += New-Object PSObject -Property #{
Instance = $PSInstance
IAResult = $PSInstance.BeginInvoke()
Argument = $PipelineObject
}
}
End {
while($True) {
$completedRunspaces = #($Runspaces | where {$_.IAResult.IsCompleted})
$completedRunspaces | foreach {
Write-Output $_.Instance.EndInvoke($_.IAResult)
$_.Instance.Dispose()
}
if($completedRunspaces.Count -eq $Runspaces.Count) {
break
}
$Runspaces = #($Runspaces | where { $completedRunspaces -notcontains $_ })
Start-Sleep -Milliseconds 250
}
$RunspacePool.Close()
$RunspacePool.Dispose()
}
}
Code partly from MathiasR.Jessen, Why PowerShell workflow is significantly slower than non-workflow script for XML file analysis
The key is to define $_ as a variable that your script block can see, via a call to Set-Variable.
Here's a simple example:
function MyNewForeachFunction {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[scriptblock] $ScriptBlock
,
[Parameter(ValueFromPipeline)]
$InputObject
)
process {
$PSInstance = [powershell]::Create()
# Add a call to define $_ based on the current pipeline input object
$null = $PSInstance.
AddCommand('Set-Variable').
AddParameter('Name', '_').
AddParameter('Value', $InputObject).
AddScript($ScriptBlock)
$PSInstance.Invoke()
}
}
# Invoke with sample values.
1, (Get-Date) | MyNewForeachFunction { "[$_]" }
The above yields something like:
[1]
[10/26/2018 00:17:37]
What I think you're looking for (and what I was looking for) is to support a "delay-bind" script block, supported in PowerShell 5.1+. The Microsoft documentation tells a bit about what's required, but doesn't provide any user-script examples (currently).
The gist is that PowerShell will implicitly detect that your function can accept a delay-bind script block if it defines an explicitly typed pipeline parameter (either by Value or by PropertyName), as long as it's not of type [scriptblock] or type [object].
function Test-DelayedBinding {
param(
# this is our typed pipeline parameter
# per doc this cannot be of type [scriptblock] or [object],
# but testing shows that type [object] may be permitted
[Parameter(ValueFromPipeline, Mandatory)][string]$string,
# this is our scriptblock parameter
[Parameter(Position=0)][scriptblock]$filter
)
Process {
if (&$filter $string) {
Write-Output $string
}
}
}
# sample invocation
>'foo', 'fi', 'foofoo', 'fib' | Test-DelayedBinding { return $_ -match 'foo' }
foo
foofoo
Note that the delay-bind will only be applied if input is piped into the function, and that the script block must use named parameters (not $args) if additional parameters are desired.
The frustrating part is that there is no way to explicitly specify that delay-bind should be used, and errors resulting from incorrectly structuring your function may be non-obvious.
Maybe this can help.
I'd normally run auto-generated jobs in parallel this way:
Get-Job | Remove-Job
foreach ($param in #(3,4,5)) {
Start-Job -ScriptBlock {param($lag); sleep $lag; Write-Output "slept for $lag seconds" } -ArgumentList #($param)
}
Get-Job | Wait-Job | Receive-Job
If I understand you correctly, you are trying to get rid of param() inside the scriptblock. You may try to wrap that SB with another one. Below is the workaround for my sample:
Get-Job | Remove-Job
#scriptblock with no parameter
$job = { sleep $lag; Write-Output "slept for $lag seconds" }
foreach ($param in #(3,4,5)) {
Start-Job -ScriptBlock {param($param, $job)
$lag = $param
$script = [string]$job
Invoke-Command -ScriptBlock ([Scriptblock]::Create($script))
} -ArgumentList #($param, $job)
}
Get-Job | Wait-Job | Receive-Job
# I was looking for an easy way to do this in a scripted function,
# and the below worked for me in PSVersion 5.1.17134.590
function Test-ScriptBlock {
param(
[string]$Value,
[ScriptBlock]$FilterScript={$_}
)
$_ = $Value
& $FilterScript
}
Test-ScriptBlock -Value 'unimportant/long/path/to/foo.bar' -FilterScript { [Regex]::Replace($_,'unimportant/','') }
The title says it all single boolean value becomes an array when assigned to a NoteProperty using Add-Member or using splatting.
PSVersion: 5.0.1xx
I have what I consider a strange problem. I am creating a PSObject with one of the NoteProperty members as a boolean. The function loops through a list, calls a function to perform an evaluation, creates an object and then adds it to an array. This seems to only happen to the first object created but I have not tested this with 5 or more objects being created.
I have validated that the functions are actually returning bool and that the variable being assigned to the property is an bool.
My workaround seems solid but am curious as to why this is happening.
Here's part of the code:
$clientCertsRequired = Get-Is-Client-Cert-Required -configFile $configFile -siteName $siteName
$httpsStatus = "Https is not enabled"
$clientCertStatus = "Client certs are not required"
if ($httpsEnabled -eq $true) {
$httpsStatus = "Https is enabled"
}
if ($clientCertsRequired -eq $true){
$clientCertStatus = "Client certs are required"
}
$sc = New-Object PSObject -Property #{
SiteName = $siteName;
ConfigFilePath = $path;
HttpsEnabled = $httpsStatus;
ClientCertStatus =$clientCertStatus;
ClientCertRequired = $clientCertsRequired;
}
# clean up of some inexplicable problem where assignment to property
# produces array with actual value in the last element.
if ($sc.ClientCertRequired.GetType().Name -eq "Object[]"){
$sc.ClientCertRequired = $sc.ClientCertRequired[-1]
}
$si += $sc
Function Get-Is-Client-Cert-Required{
param(
[xml]$configFile,
[string]$siteName
)
$functionName = $MyInvocation.MyCommand.Name
$clientCertRequired = $false
try{
# then read locations section (this will often not have any pages
$locationPath = "//configuration/location[#path='$siteName']"
[system.xml.xmlelement]$location = $configFile.DocumentElement.SelectSingleNode($locationPath)
if($location -ne $null){
[system.xml.xmlelement]$accessNode = $location.SelectSingleNode("system.webServer/security/access")
[system.xml.xmlelement]$authenticationNode = $location.SelectSingleNode("system.webServer/security/authentication")
[system.xml.xmlelement]$clientCertMappingNode
[system.xml.xmlelement]$iisClientCertMappingNode
[int]$sslFlagMask = 0
if($accessNode -ne $null){
$sslFlags = $accessNode.Attributes.GetNamedItem("sslFlags")
# $sslFlags = $accessNode.Attributes["sslFlags"].Value
if($sslFlagMask -ne $null){
$sslFlagMask = Convert-Ssl-Flag-String-To-Int-Flag -sslFlag $sslFlags.Value
}
}
if($authenticationNode -ne $null){
[system.xml.xmlelement]$clientCertMappingNode = $authenticationNode.SelectSingleNode("clientCertificateMappingAuthentication[#enabled='true']")
[system.xml.xmlelement]$iisClientCertMappingNode = $authenticationNode.SelectSingleNode("iisClientCertificateMappingAuthentication[#enabled='true']")
}
$clientCertAccepted = ($sslFlagMask -band $certAccepted) -eq $certAccepted
$clientCertRequired = Check-IIS-Express-SSL-Config $sslFlagMask
if($clientCertRequired -eq $false){
if($clientCertAccepted -and ($clientCertMappingNode -ne $null -or $iisClientCertMappingNode -ne $null)){
$clientCertRequired = $true
}
}
}
}catch{
$exceptionMessage = Get-Formatted-Exception-String -exceptionObject $_
$message = "$functionName - Exception`: $exceptionMessage"
Add-Exception -exception $message
Log-Error -message $message
}
$clientCertRequired
}
In the body of the Get-Is-Client-Cert-Required function, you do:
[system.xml.xmlelement]$clientCertMappingNode
[system.xml.xmlelement]$iisClientCertMappingNode
This pattern:
[type]$nonExistingVariable
Is a terrible idea in PowerShell - unlike C#, PowerShell does not have the concept of bare variable declarations, and the above pattern simply casts $null to the specified type, emitting a new instance of said type if it succeeds - this is likely what causes the function to output an array.
If you really need to bind a variable to a specific type, cast on assignment:
[type]$Variable = Get-Stuff
Bonus tip: The PowerShell-idiomatic naming convention for functions and cmdlets is Noun-Verb, with only a single hyphen. A more appropriate name for the function would be:
Test-ClientCertRequirement
How can you test if an object has a specific property?
Appreciate I can do ...
$members = Get-Member -InputObject $myobject
and then foreach through the $members, but is there a function to test if the object has a specific property?
Additional Info:
The issue is I'm importing two different sorts of CSV file, one with two columns, the other with three. I couldn't get the check to work with "Property", only with "NoteProperty" ... whatever the difference is
if ( ($member.MemberType -eq "NoteProperty" ) -and ($member.Name -eq $propertyName) )
Like this?
[bool]($myObject.PSobject.Properties.name -match "myPropertyNameToTest")
You can use Get-Member
if (Get-Member -inputobject $var -name "Property" -Membertype Properties) {
#Property exists
}
This is succinct and readable:
"MyProperty" -in $MyObject.PSobject.Properties.Name
We can put it in a function:
function HasProperty($object, $propertyName)
{
$propertyName -in $object.PSobject.Properties.Name
}
For me MyProperty" -in $MyObject.PSobject.Properties.Name didn't work, however
$MyObject.PSobject.Properties.Name.Contains("MyProperty")
works
There are a number of solutions to this question that work in strict mode, but some are better than others.
Solutions that do not appear to iterate through every property are the fastest solutions.
Bernie White's solution and
esskar's solution (modified)
Solutions that look as though they iterate through every property are slower.
sebke CCU's solution and
dan-gph's solution
The solution that appears to iterate through every property and uses a regular expression is a little slower than the previous two solutions (because compiling and executing the regular expression takes more time)
CB.'s solution
The solution that uses GetMethod appears to iterate through every property, but its use of GetMethod makes it significantly slower.
Paul's GetMethod solution
The following script was used to compare the previously mentioned solutions in strict mode:
# Tested in PowerShell core 7.2.0
Set-StrictMode -Version Latest
$propertyExistsMethods = New-Object System.Collections.Generic.Dictionary'[string,scriptblock]'
# Fastest
$propertyExistsMethods.Add(
"PSObject.Properties (Bernie White's solution)",
{
Param( [PSObject] $Object, [string] $Property )
[bool]$Object.PSObject.Properties[$Property]
})
$propertyExistsMethods.Add(
"PSObject.Properties.Item (esskar's solution (modified))",
{
Param( [PSObject] $Object, [string] $Property )
[bool]$Object.PSObject.Properties.Item($property)
})
# Not as fast
$propertyExistsMethods.Add(
"Contains (sebke CCU's solution)",
{
Param( [PSObject] $Object, [string] $Property )
$Object.PSobject.Properties.Name.Contains($Property)
})
$propertyExistsMethods.Add(
"-in (dan-gph's solution)",
{
Param( [PSObject] $Object, [string] $Property )
$Property -in $Object.PSobject.Properties.Name
})
# Slower than the previously mentioned solutions
$propertyExistsMethods.Add(
"-match (CB.'s solution)",
{
Param( [PSObject] $Object, [string] $Property )
[bool]($Object.PSobject.Properties.name -match $Property)
})
# Slowest
$propertyExistsMethods.Add(
"GetMember (Paul's solution)",
{
Param( [PSObject] $Object, [string] $Property )
Get-Member -inputobject $Object -name $Property -Membertype Properties
})
foreach ($method in $propertyExistsMethods.Keys) {
$propertyExists = $propertyExistsMethods[$method]
$o = #{}
foreach ($i in 1..100000) {
$o[$i] = "p$i"
}
Write-Host $method
$measure = Measure-Command {
foreach ($i in 1..100000) {
# Always check for a property that does NOT exist
& $propertyExists -Object $o -Property 'p'
}
}
Write-Host $measure | % { $_.Milliseconds }
Write-Host ''
}
The output is as follows:
PSObject.Properties (Bernie White's solution)
00:00:03.1437587
PSObject.Properties.Item (esskar's solution)
00:00:03.5833642
Contains (sebke CCU's solution)
00:00:04.4812702
-in (dan-gph's solution)
00:00:04.6507811
-match (CB.'s solution)
00:00:05.1107066
GetMember (Paul's solution)
00:00:14.5305115
Try this for a one liner that is strict safe.
[bool]$myobject.PSObject.Properties[$propertyName]
For example:
Set-StrictMode -Version latest;
$propertyName = 'Property1';
$myobject = [PSCustomObject]#{ Property0 = 'Value0' };
if ([bool]$myobject.PSObject.Properties[$propertyName]) {
$value = $myobject.$propertyName;
}
I've been using the following which returns the property value, as it would be accessed via $thing.$prop, if the "property" would be to exist and not throw a random exception. If the property "doesn't exist" (or has a null value) then $null is returned: this approach functions in/is useful for strict mode, because, well, Gonna Catch 'em All.
I find this approach useful because it allows PS Custom Objects, normal .NET objects, PS HashTables, and .NET collections like Dictionary to be treated as "duck-typed equivalent", which I find is a fairly good fit for PowerShell.
Of course, this does not meet the strict definition of "has a property".. which this question may be explicitly limited to. If accepting the larger definition of "property" assumed here, the method can be trivially modified to return a boolean.
Function Get-PropOrNull {
param($thing, [string]$prop)
Try {
$thing.$prop
} Catch {
}
}
Examples:
Get-PropOrNull (Get-Date) "Date" # => Monday, February 05, 2018 12:00:00 AM
Get-PropOrNull (Get-Date) "flub" # => $null
Get-PropOrNull (#{x="HashTable"}) "x" # => "HashTable"
Get-PropOrNull ([PSCustomObject]#{x="Custom"}) "x" # => "Custom"
$oldDict = New-Object "System.Collections.HashTable"
$oldDict["x"] = "OldDict"
Get-PropOrNull $d "x" # => "OldDict"
And, this behavior might not [always] be desired.. ie. it's not possible to distinguish between x.Count and x["Count"].
Just check against null.
($myObject.MyProperty -ne $null)
If you have not set PowerShell to StrictMode, this works even if the property does not exist:
$obj = New-Object PSObject;
Add-Member -InputObject $obj -MemberType NoteProperty -Name Foo -Value "Bar";
$obj.Foo; # Bar
($obj.MyProperty -ne $null); # False, no exception
If you are using StrictMode and the psobject might be empty, it will give you an error.
For all purposes this will do:
if (($json.PSobject.Properties | Foreach {$_.Name}) -contains $variable)
I find this method more strict and faster when checking multiple properties
$null -ne $myobject.PSObject.Properties.Item("myPropertyNameToTest")
Real similar to a javascript check:
foreach($member in $members)
{
if($member.PropertyName)
{
Write $member.PropertyName
}
else
{
Write "Nope!"
}
}
Just to clarify
given the following object
$Object
With the following properties
type : message
user : john.doe#company.com
text :
ts : 11/21/2016 8:59:30 PM
The following are true
$Object.text -eq $NULL
$Object.NotPresent -eq $NULL
-not $Object.text
-not $Object.NotPresent
So the earlier answers that explicitly check for the property by name is the most correct way to verify that that property is not present.
I ended up with the following function ...
function HasNoteProperty(
[object]$testObject,
[string]$propertyName
)
{
$members = Get-Member -InputObject $testObject
if ($members -ne $null -and $members.count -gt 0)
{
foreach($member in $members)
{
if ( ($member.MemberType -eq "NoteProperty" ) -and `
($member.Name -eq $propertyName) )
{
return $true
}
}
return $false
}
else
{
return $false;
}
}
I recently switch to set strict-mode -version 2.0 and my null tests failed.
I added a function:
#use in strict mode to validate property exists before using
function exists {
param($obj,$prop)
try {
if ($null -ne $obj[$prop]) {return $true}
return $false
} catch {
return $false
}
return $false
}
Now I code
if (exists $run main) { ...
rather than
if ($run.main -ne $null) { ...
and we are on our way. Seems to work on objects and hashtables
As an unintended benefit it is less typing.
for me this work
Set-StrictMode -Version Latest
$TMP = ...
$HAS_SERVERS=($TMP | Select-Object Servers)
if (-not $HAS_SERVERS.Servers){
echo "No servers. Abort."
} else {
...
}
I just started using PowerShell with PowerShell Core 6.0 (beta) and following simply works:
if ($members.NoteProperty) {
# NoteProperty exist
}
or
if (-not $members.NoteProperty) {
# NoteProperty does not exist
}
You could check with:
($Member.PropertyNames -contains "Name") this will check for the Named property
For identifying which of the objects in an array have a property
$HasProperty = $ArrayOfObjects | Where-Object {$_.MyProperty}
How do you implement the parameter tab completion for PowerShell functions or cmdlets like Get-Service and Get-Process in PowerShell 3.0?
I realise ValidateSet works for a known list, but I want to generate the list on demand.
Adam Driscoll hints that it is possible for cmdlets but unfortunately hasn't elaborated.
Trevor Sullivan shows a technique for functions, but as I understand it, his code only generates the list at the time the function is defined.
I puzzled over this for a while, because I wanted to do the same thing. I put together something that I'm really happy with.
You can add ValidateSet attributes from a DynamicParam. Here's an example of where I've generated my ValidateSet on-the-fly from an xml file. See the "ValidateSetAttribute" in the following code:
function Foo() {
[CmdletBinding()]
Param ()
DynamicParam {
#
# The "modules" param
#
$modulesAttributeCollection = new-object -Type System.Collections.ObjectModel.Collection[System.Attribute]
# [parameter(mandatory=...,
# ...
# )]
$modulesParameterAttribute = new-object System.Management.Automation.ParameterAttribute
$modulesParameterAttribute.Mandatory = $true
$modulesParameterAttribute.HelpMessage = "Enter one or more module names, separated by commas"
$modulesAttributeCollection.Add($modulesParameterAttribute)
# [ValidateSet[(...)]
$moduleNames = #()
foreach($moduleXmlInfo in Select-Xml -Path "C:\Path\to\my\xmlFile.xml" -XPath "//enlistment[#name=""wp""]/module") {
$moduleNames += $moduleXmlInfo.Node.Attributes["name"].Value
}
$modulesValidateSetAttribute = New-Object -type System.Management.Automation.ValidateSetAttribute($moduleNames)
$modulesAttributeCollection.Add($modulesValidateSetAttribute)
# Remaining boilerplate
$modulesRuntimeDefinedParam = new-object -Type System.Management.Automation.RuntimeDefinedParameter("modules", [String[]], $modulesAttributeCollection)
$paramDictionary = new-object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
$paramDictionary.Add("modules", $modulesRuntimeDefinedParam)
return $paramDictionary
}
process {
# Do stuff
}
}
With that, I can type
Foo -modules M<press tab>
and it will tab-complete "MarcusModule" if that module was in the XML file. Furthermore, I can edit the XML file and the tab-completion behavior will immediately change; you don't have to re-import the function.
Check the TabExpansionPlusPlus module on github, written by a former PowerShell team magician.
https://github.com/lzybkr/TabExpansionPlusPlus#readme
Classically, I used regex.
for example,
function TabExpansion {
param($line, $lastWord)
if ( $line -match '(-(\w+))\s+([^-]*$)' )
{
### Resolve Command name & parameter name
$_param = $matches[2] + '*'
$_opt = $Matches[3].Split(" ,")[-1] + '*'
$_base = $Matches[3].Substring(0,$Matches[3].Length-$Matches[3].Split(" ,")[-1].length)
$_cmdlet = [regex]::Split($line, '[|;=]')[-1]
if ($_cmdlet -match '\{([^\{\}]*)$')
{
$_cmdlet = $matches[1]
}
if ($_cmdlet -match '\(([^()]*)$')
{
$_cmdlet = $matches[1]
}
$_cmdlet = $_cmdlet.Trim().Split()[0]
$_cmdlet = #(Get-Command -type 'Cmdlet,Alias,Function,Filter,ExternalScript' $_cmdlet)[0]
while ($_cmdlet.CommandType -eq 'alias')
{
$_cmdlet = #(Get-Command -type 'Cmdlet,Alias,Function,Filter,ExternalScript' $_cmdlet.Definition)[0]
}
### Currently target is Get-Alias & "-Name" parameter
if ( "Get-Alias" -eq $_cmdlet.Name -and "Name" -like $_param )
{
Get-Alias -Name $_opt | % { $_.Name } | sort | % { $_base + ($_ -replace '\s','` ') }
break;
}
}
}
Reference
http://gallery.technet.microsoft.com/scriptcenter/005d8bc7-5163-4a25-ad0d-25cffa90faf5
Posh-git renames TabExpansion to TabExpansionBackup in GitTabExpansion.ps1.
And posh-git's redifined TabExpansion calls original TabExpansion(TabExpansionBackup) when completions don't match with git commands.
So all you have to do is redefine TabExpansionBackup.
(cat .\GitTabExpansion.ps1 | select -last 18)
============================== GitTabExpansion.ps1 ==============================
if (Test-Path Function:\TabExpansion) {
Rename-Item Function:\TabExpansion TabExpansionBackup
}
function TabExpansion($line, $lastWord) {
$lastBlock = [regex]::Split($line, '[|;]')[-1].TrimStart()
switch -regex ($lastBlock) {
# Execute git tab completion for all git-related commands
"^$(Get-AliasPattern git) (.*)" { GitTabExpansion $lastBlock }
"^$(Get-AliasPattern tgit) (.*)" { GitTabExpansion $lastBlock }
# Fall back on existing tab expansion
default { if (Test-Path Function:\TabExpansionBackup) { TabExpansionBackup $line $lastWord } }
}
}
===============================================================================
Redefine TabExpansionBackup(original TabExpansion)
function TabExpansionBackup {
...
### Resolve Command name & parameter name
...
### Currently target is Get-Alias & "-Name" parameter
...
}
Say I have MyScript.ps1:
[cmdletbinding()]
param (
[Parameter(Mandatory=$true)]
[string] $MyInput
)
function Show-Input {
param ([string] $Incoming)
Write-Output $Incoming
}
function Save-TheWorld {
#ToDo
}
Write-Host (Show-Input $MyInput)
Is it possible to dot source the functions only somehow? The problem is that if the script above is dot sourced, it executes the whole thing...
Is my best option to use Get-Content and parse out the functions and use Invoke-Expression...? Or is there a way to access PowerShell's parser programmatically? I see this might be possible with PSv3 using [System.Management.Automation.Language.Parser]::ParseInput but this isn't an option because it has to work on PSv2.
The reason why I'm asking is that i'm trying out the Pester PowerShell unit testing framework and the way it runs tests on functions is by dot sourcing the file with the functions in the test fixture. The test fixture looks like this:
MyScript.Tests.ps1
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path).Replace(".Tests.", ".")
. "$here\$sut"
Describe "Show-Input" {
It "Verifies input 'Hello' is equal to output 'Hello'" {
$output = Show-Input "Hello"
$output.should.be("Hello")
}
}
Using Doug's Get-Function function you could include the functions this way:
$script = get-item .\myscript.ps1
foreach ($function in (get-function $script))
{
$startline = $function.line - 1
$endline = $startline
$successful = $false
while (! $successful)
{
try {
$partialfunction = ((get-content $script)[$startline..$endline]) -join [environment]::newline
invoke-expression $partialfunction
$successful = $true
}
catch [Exception] { $endline++ }
}
}
Edit: [System.Management.Automation.IncompleteParseException] can be used instead of [Exception] in Powershell V2.
Note -- if you find this answer helpful please upvote jonZ's answer as I wouldn't of been able to come up with this if it weren't for his helpful answer.
I created this function extractor function based on the script #jonZ linked to. This uses [System.Management.Automation.PsParser]::Tokenize to traverse all tokens in the input script and parses out functions into function info objects and returns all function info objects as an array. Each object looks like this:
Start : 99
Stop : 182
StartLine : 7
Name : Show-Input
StopLine : 10
StartColumn : 5
StopColumn : 1
Text : {function Show-Input {, param ([string] $Incoming), Write-Output $Incoming, }}
The text property is a string array and can be written to temporary file and dot sourced in or combined into a string using a newline and imported using Invoke-Expression.
Only the function text is extracted so if a line has multiple statements such as: Get-Process ; function foo () { only the part relevant to the function will be extracted.
function Get-Functions {
param (
[Parameter(Mandatory=$true)]
[System.IO.FileInfo] $File
)
try {
$content = Get-Content $File
$PSTokens = [System.Management.Automation.PsParser]::Tokenize($content, [ref] $null)
$functions = #()
#Traverse tokens.
for ($i = 0; $i -lt $PSTokens.Count; $i++) {
if($PSTokens[$i].Type -eq 'Keyword' -and $PSTokens[$i].Content -eq 'Function' ) {
$fxStart = $PSTokens[$i].Start
$fxStartLine = $PSTokens[$i].StartLine
$fxStartCol = $PSTokens[$i].StartColumn
#Skip to the function name.
while (-not ($PSTokens[$i].Type -eq 'CommandArgument')) {$i++}
$functionName = $PSTokens[$i].Content
#Skip to the start of the function body.
while (-not ($PSTokens[$i].Type -eq 'GroupStart') -and -not ($PSTokens[$i].Content -eq '{')) {$i++ }
#Skip to the closing brace.
$startCount = 1
while ($startCount -gt 0) { $i++
if ($PSTokens[$i].Type -eq 'GroupStart' -and $PSTokens[$i].Content -eq '{') {$startCount++}
if ($PSTokens[$i].Type -eq 'GroupEnd' -and $PSTokens[$i].Content -eq '}') {$startCount--}
}
$fxStop = $PSTokens[$i].Start
$fxStopLine = $PSTokens[$i].StartLine
$fxStopCol = $PSTokens[$i].StartColumn
#Extract function text. Handle 1 line functions.
$fxText = $content[($fxStartLine -1)..($fxStopLine -1)]
$origLine = $fxText[0]
$fxText[0] = $fxText[0].Substring(($fxStartCol -1), $fxText[0].Length - ($fxStartCol -1))
if ($fxText[0] -eq $fxText[-1]) {
$fxText[-1] = $fxText[-1].Substring(0, ($fxStopCol - ($origLine.Length - $fxText[0].Length)))
} else {
$fxText[-1] = $fxText[-1].Substring(0, ($fxStopCol))
}
$fxInfo = New-Object -TypeName PsObject -Property #{
Name = $functionName
Start = $fxStart
StartLine = $fxStartLine
StartColumn = $fxStartCol
Stop = $fxStop
StopLine = $fxStopLine
StopColumn = $fxStopCol
Text = $fxText
}
$functions += $fxInfo
}
}
return $functions
} catch {
throw "Failed in parse file '{0}'. The error was '{1}'." -f $File, $_
}
}
# Dumping to file and dot sourcing:
Get-Functions -File C:\MyScript.ps1 | Select -ExpandProperty Text | Out-File C:\fxs.ps1
. C:\fxs.ps1
Show-Input "hi"
#Or import without dumping to file:
Get-Functions -File C:\MyScript.ps1 | % {
$_.Text -join [Environment]::NewLine | Invoke-Expression
}
Show-Input "hi"