Powershell: Multiple parameters for a TabExpansion++ ArgumentCompleter - powershell

I am working on a function to schedule a user's home drive transfer, I am going to use TabExpansion++ to allow the user to autocomplete the server name, which is populated from a CSV file. There will be parameters for both OldServer and NewServer.
Is it possible with TabExpansion++ to specify more than one parameter for a single autocompleter?
Here is what I have:
function HomeDriveSiteCompletion {
[ArgumentCompleter(
Parameter = 'OldServer',
Command = { 'Schedule-HomeTransfer' },
Description = 'Home drive transfer tool server name autocomplete')]
param($commandName,$parameterName,$wordToComplete,$commandAst,$fakeBoundParameter)
Import-Csv -Path $Global:ServersList | % {New-CompletionResult -ToolTip $_.Site -completiontext $_.Site}
}
Which works fine for OldServer. If I can save code by specifying both parameters in the same place, that would be ideal. I have tried both
Parameter = #('OldServer','NewServer')
and
Parameter = { 'OldServer','NewServer' }
Neither of which worked. Is there another way I could make this work?

Questions like this are why I love this site. I have not used TabExpansion++, but I have done some tab expansion stuff for parameters. I couldn't remember if I'd run into this exact question before so I went looking and discovered something that I haven't encountered in the PowerShell world before, DynamicParam. How have I not seen this before? The levels of awesome of it for situations like this are right off the charts! What it allows you to do is not declare a parameter, but then add that parameter before the actual scriptblock of the function, and do scripty kinds of things for validation of that parameter.
I asked Google for a little help, and it pointed me to this SO question (where Shay Levy gives the accepted answer recommending TabExpansion++), but the next answer goes on about DynamicParam. So I looked that up and found this blog on Microsoft's site that explains it further. Basically for your needs you would do something like:
DynamicParam {
$SrvList = Import-CSV $Global:ServerList | Select -Expand Site
$ParamNames = #('OldServer','NewServer')
#Create Param Dictionary
$ParamDictionary = new-object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
ForEach($Name in $ParamNames){
#Create a container for the new parameter's various attributes, like Manditory, HelpMessage, etc that usually goes in the [Parameter()] part
$ParamAttribCollecton = new-object -Type System.Collections.ObjectModel.Collection[System.Attribute]
#Create each attribute
$ParamAttrib = new-object System.Management.Automation.ParameterAttribute
$ParamAttrib.Mandatory = $true
$ParamAttrib.HelpMessage = "Enter a server name"
#Create ValidationSet to make tab-complete work
$ParamValSet = New-Object -type System.Management.Automation.ValidateSetAttribute($SrvList)
#Add attributes and validationset to the container
$ParamAttribCollecton.Add($ParamAttrib)
$ParamAttribCollecton.Add($ParamValSet)
#Create the actual parameter, then add it to the Param Dictionary
$MyParam = new-object -Type System.Management.Automation.RuntimeDefinedParameter($Name, [String], $ParamAttribCollecton)
$ParamDictionary.Add($Name, $MyParam)
}
#Return the param dictionary so the function can add the parameters to itself
return $ParamDictionary
}
That would add the OldServer and NewServer parameters to your function. Both would tab-complete the servers listed in the Site column of the CSV located at $global:ServerList. Sure, it's not as short and sweet as TabExpansion++'s context, but on the other hand it does not require any additional modules or anything to be loaded on the system since it is all self contained and only using basic PowerShell features.
Now, that adds the parameters, but it doesn't actually assign them to variables, so we'll have to do that in the Begin part of the function. We'll list the parameters in PSBoundParameters.Keys and check if a variable already exists in the current scope, and if not we'll make one in the current scope, so as to mess with anything outside of the function. So, with a basic parameter of -User, the two dynamic parameters, and the addition of the variables for the dynamic parameters, we're looking at something like this for your function:
Function Schedule-HomeTransfer{
[CmdletBinding()]
Param([string]$User)
DynamicParam {
$SrvList = Import-CSV $Global:ServerList | Select -Expand Site
$ParamNames = #('OldServer','NewServer')
#Create Param Dictionary
$ParamDictionary = new-object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
ForEach($Name in $ParamNames){
#Create a container for the new parameter's various attributes, like Manditory, HelpMessage, etc that usually goes in the [Parameter()] part
$ParamAttribCollecton = new-object -Type System.Collections.ObjectModel.Collection[System.Attribute]
#Create each attribute
$ParamAttrib = new-object System.Management.Automation.ParameterAttribute
$ParamAttrib.Mandatory = $true
$ParamAttrib.HelpMessage = "Enter a server name"
#Create ValidationSet to make tab-complete work
$ParamValSet = New-Object -type System.Management.Automation.ValidateSetAttribute($SrvList)
#Add attributes and validationset to the container
$ParamAttribCollecton.Add($ParamAttrib)
$ParamAttribCollecton.Add($ParamValSet)
#Create the actual parameter, then add it to the Param Dictionary
$MyParam = new-object -Type System.Management.Automation.RuntimeDefinedParameter($Name, [String], $ParamAttribCollecton)
$ParamDictionary.Add($Name, $MyParam)
}
#Return the param dictionary so the function can add the parameters to itself
return $ParamDictionary
}
Begin{$PSBoundParameters.Keys | Where{!(Get-Variable -name $_ -Scope 0 -ErrorAction SilentlyContinue)} | ForEach{New-Variable -Name $_ -Value $PSBoundParameters[$_]}}
Process{
"You chose to move $User from $OldServer to $NewServer"
}
}
That right there will allow for tab completion on -OldServer and -NewServer, and when I set $global:ServerList to "C:\Temp\new.csv' and populated that with a 'Site' column having 3 values, those popped right up for me to select (in the ISE it actually pops up a list to choose from, not just tab completion like in the console).

Related

how to create dynamic link list from .lnk files in a folder with powershell

I would like to be able to create a dynamic link list from .lnk files in a folder with powershell. I want the link list to be presented to the user in a form that can be minimzed, but will stay active for the entire session, even if the user launch one of them the main form will remain active.
I'm having a hard time moving from VBS to powershell, so any help would be appreciated.
Finally I have managed to have a begining of solution here is my code.
$linklist = #(
("MyFisrtApp" , "\\Path\To\MyFisrtApp.exe"),
("MySecondApp" , "\\Path\To\MySecondApp.exe"),
("MyThirdApp" , "\\Path\To\MyThirdApp.exe"),
("MyFourthApp" , "\\Path\To\MyFourthApp.exe")
)
#Create Form Object
$mainform = New-Object System.Windows.Forms.Form
$mainform.Size = New-Object System.Drawing.Size(400,300)
$mainform.Text = " My Virtual Applications"
$mainform.StartPosition = "CenterScreen" #loads the window in the center of the screen
# convert the array of arrays into an ordered Hashtable
$linkHash = [ordered]#{}
$linklist | ForEach-Object { $linkHash[$_[0]] = $_[1] }
$calculatedPosition =40
$linkHash.GetEnumerator() | ForEach-Object {
$lnk = New-Object System.Windows.Forms.LinkLabel
$lnk.Text = $_.Name # set the name for the label
$lnk.Tag = $_.Value # store the link url inside the control's Tag property
$lnk.Location = New-Object System.Drawing.Point(60, $calculatedPosition)
# inside the scriptblock, $this refers to the LinkLabel control itself
$lnk.Add_Click({ Start-Process $this.Tag })
$mainform.Controls.Add($lnk)
$calculatedPosition += 40 # just a guess, you may want different vertical spacing
}
#Show Form
$mainform.ShowDialog()
My next move now is to display the icon of the .lnk file on the left of each link and also dynamically construct the linklist hashtable from the properties of a list of .lnk files in a specific folder. Also having the form to auto-size as needed if more links need to be shown, would be interesting. I'm trying different setting with mitigated results.

How do I get events associated with Datastores and Datastore Clusters using PowerCLI?

This is a followup to this question I posted but running into an issue with enumerating events on some objects now. When I run the following code (or try any of the solutions in my prior question) to get the events from a datastore or datastore cluster for example:
Get-VMFolder FOLDER_NAME -Type Datastore | Get-DatastoreCluster | Get-VIEvent
I'm met with the following error for each event it tries to return:
Events can be retrieved only for inventory objects. The entity of type
'VMware.VimAutomation.ViCore.Impl.V1.DatastoreManagement.VmfsDatastoreImpl' will be ignored.
This is particularly annoying since the cmdlet clearly enumerates the events as I get this error for every event it attempts to return.
When I use the Get-TaskPlus function mentioned in the accepted answer to my prior question returns a different type conversion error:
Cannot process argument transformation on parameter 'Entity'. Cannot convert the "DATASTORE_CLUSTER_NAME" value of type "VMware.VimAutomation.ViCore.Impl.V1.DatastoreManagement.DatastoreClusterImpl" to type "VMware.VimAutomation.ViCore.Impl.V1.Inventory.InventoryItemImpl".
If I remove the type constraint on the $Entity argument in the function definition, the error goes away but I also don't get any results.
I'm not really looking for recommendations or tools here, but if Get-VIEvent to look for events on non-inventory objects through PowerCLI, is there a workaround or more nuanced way to retrieve this information with PowerCLI?
After I posted this I did some more digging and found that Luc Dekens wrote another function calledGet-VIEventPlus. This doesn't work out of the box because -Entity expects a type of VMware.VimAutomation.ViCore.Impl.V1.Inventory.InventoryItemImpl[], but datastores and datastore clusters have a different type under the VMWare.VimAutomation.ViCore.Impl.V1.DatastoreManagement namespace.
If we make one change to LucD's function to accept the base type VMware.VimAutomation.ViCore.Impl.V1.VIObjectImpl[] instead of InventoryItemImpl[], Get-VIEventPlus should work other vCenter object types:
function Get-VIEventPlus {
<#
.SYNOPSIS Returns vSphere events
.DESCRIPTION The function will return vSphere events. With
the available parameters, the execution time can be
improved, compered to the original Get-VIEvent cmdlet.
.NOTES Author: Luc Dekens
.PARAMETER Entity
When specified the function returns events for the
specific vSphere entity. By default events for all
vSphere entities are returned.
.PARAMETER EventType
This parameter limits the returned events to those
specified on this parameter.
.PARAMETER Start
The start date of the events to retrieve
.PARAMETER Finish
The end date of the events to retrieve.
.PARAMETER Recurse
A switch indicating if the events for the children of
the Entity will also be returned
.PARAMETER User
The list of usernames for which events will be returned
.PARAMETER System
A switch that allows the selection of all system events.
.PARAMETER ScheduledTask
The name of a scheduled task for which the events
will be returned
.PARAMETER FullMessage
A switch indicating if the full message shall be compiled.
This switch can improve the execution speed if the full
message is not needed.
.EXAMPLE
PS> Get-VIEventPlus -Entity $vm
.EXAMPLE
PS> Get-VIEventPlus -Entity $cluster -Recurse:$true
#>
param(
[VMware.VimAutomation.ViCore.Impl.V1.VIObjectImpl[]]$Entity,
[string[]]$EventType,
[DateTime]$Start,
[DateTime]$Finish = (Get-Date),
[switch]$Recurse,
[string[]]$User,
[Switch]$System,
[string]$ScheduledTask,
[switch]$FullMessage = $false
)
process {
$eventnumber = 100
$events = #()
$eventMgr = Get-View EventManager
$eventFilter = New-Object VMware.Vim.EventFilterSpec
$eventFilter.disableFullMessage = ! $FullMessage
$eventFilter.entity = New-Object VMware.Vim.EventFilterSpecByEntity
$eventFilter.entity.recursion = &{if($Recurse){"all"}else{"self"}}
$eventFilter.eventTypeId = $EventType
if($Start -or $Finish){
$eventFilter.time = New-Object VMware.Vim.EventFilterSpecByTime
if($Start){
$eventFilter.time.beginTime = $Start
}
if($Finish){
$eventFilter.time.endTime = $Finish
}
}
if($User -or $System){
$eventFilter.UserName = New-Object VMware.Vim.EventFilterSpecByUsername
if($User){
$eventFilter.UserName.userList = $User
}
if($System){
$eventFilter.UserName.systemUser = $System
}
}
if($ScheduledTask){
$si = Get-View ServiceInstance
$schTskMgr = Get-View $si.Content.ScheduledTaskManager
$eventFilter.ScheduledTask = Get-View $schTskMgr.ScheduledTask |
where {$_.Info.Name -match $ScheduledTask} |
Select -First 1 |
Select -ExpandProperty MoRef
}
if(!$Entity){
$Entity = #(Get-Folder -Name Datacenters)
}
$entity | %{
$eventFilter.entity.entity = $_.ExtensionData.MoRef
$eventCollector = Get-View ($eventMgr.CreateCollectorForEvents($eventFilter))
$eventsBuffer = $eventCollector.ReadNextEvents($eventnumber)
while($eventsBuffer){
$events += $eventsBuffer
$eventsBuffer = $eventCollector.ReadNextEvents($eventnumber)
}
$eventCollector.DestroyCollector()
}
$events
}
}
Update: The original answer changed the -Entity type to be an object[]. I have updated this function to make use of the VMware.VimAutomation.ViCore.Impl.V1.VIObjectImpl[] base type for -Entity instead.

Powershell - pass a value to parameter

How to pass value along with parameter? Something like ./test.ps1 -controllers 01. I want the script to use hyphen and also a value is passed along for the parameter.
Here is the part of the script I wrote. But if I call the script with hyphen (.\test.ps1 -Controllers) it says A parameter cannot be found that matches parameter name 'Controllers'.
param(
# [Parameter(Mandatory=$false, Position=0)]
[ValidateSet('Controllers','test2','test3')]
[String]$options
)
Also I need to pass a value to it which is then used for a property.
if ($options -eq "controllers")
{
$callsomething.$arg1 | where {$_ -eq "$arg2" }
}
Lets talk about why it does not work
function Test()
param(
[Parameter(Mandatory=$false, Position=0)]
[ValidateSet('Controllers','test2','test3')]
[String]$options
)
}
Parameters are Variables that are created and filled out at the start of the script
ValidateSet will only allow the script to run if $Options equals one of the three choices 'Controllers','test2','test3'
Lets talk about what exactly all the [] are doing
Mandatory=$false means that $options doesnt have to be anything in order for the script to run.
Position=0 means that if you entered the script without using the -options then the very first thing you put would still be options
Example
#If Position=0 then this would work
Test "Controllers"
#Also this would work
Test -options Controllers
[ValidateSet('Controllers','test2','test3')] means that if Option is used or is Mandatory then it has to equal 'Controllers','test2','test3'
It sounds like you are trying to create parameters at runtime. Well that is possible using DynamicParam.
function Test{
[CmdletBinding()]
param()
DynamicParam {
$Parameters = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
'Controllers','test2','test3' | Foreach-object{
$Param = New-Object System.Management.Automation.ParameterAttribute
$Param.Mandatory = $false
$AttribColl = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$AttribColl.Add($Param)
$RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter("$_", [string], $AttribColl)
$Parameters.Add("$_", $RuntimeParam)
}
return $Parameters
}
begin{
$PSBoundParameters.GetEnumerator() | ForEach-Object{
Set-Variable $_.Key -Value $_.Value
}
}
process {
"$Controllers $Test2 $Test3"
}
}
DynamicParam allows you to create parameters in code.
The example above turns the array 'Controllers','test2','test3' into 3 separate parameters.
Test -Controllers "Hello" -test2 "Hey" -test3 "Awesome"
returns
Hello Hey Awesome
But you said you wanted to keep the hypen and the parameter
So the line
$PSBoundParameters.GetEnumerator() | ForEach-Object{
Set-Variable $_.Key -Value $_.Value
}
allows you to define each parameter value. a slight change like :
$PSBoundParameters.GetEnumerator() | ForEach-Object{
Set-Variable $_.Key -Value "-$($_.Key) $($_.Value)"
}
Would return
-Controllers Hello -test2 Hey -test3 Awesome

Is there a using namespace equivalent pre-version 5? [duplicate]

When I use another object in the .net-Framework in C# I can save a lot of typing by using the using directive.
using FooCompany.Bar.Qux.Assembly.With.Ridiculous.Long.Namespace.I.Really.Mean.It;
...
var blurb = new Thingamabob();
...
So is there a way in Powershell to do something similiar? I'm accessing a lot of .net objects and am not happy of having to type
$blurb = new-object FooCompany.Bar.Qux.Assembly.With.Ridiculous.Long.Namespace.I.Really.Mean.It.Thingamabob;
all the time.
There's really nothing at the namespace level like that. I often assign commonly used types to variables and then instantiate them:
$thingtype = [FooCompany.Bar.Qux.Assembly.With.Ridiculous.Long.Namespace.I.Really.Mean.It.Thingamabob];
$blurb = New-Object $thingtype.FullName
Probably not worth it if the type won't be used repeatedly, but I believe it's the best you can do.
PowerShell 5.0 (included in WMF5 or Windows 10 and up), adds the using namespace construct to the language. You can use it in your script like so:
#Require -Version 5.0
using namespace FooCompany.Bar.Qux.Assembly.With.Ridiculous.Long.Namespace.I.Really.Mean.It
$blurb = [Thingamabob]::new()
(The #Require statement on the first line is not necessary to use using namespace, but it will prevent the script from running in PS 4.0 and below where using namespace is a syntax error.)
Check out this blog post from a couple years ago: http://blogs.msdn.com/richardb/archive/2007/02/21/add-types-ps1-poor-man-s-using-for-powershell.aspx
Here is add-types.ps1, excerpted from that article:
param(
[string] $assemblyName = $(throw 'assemblyName is required'),
[object] $object
)
process {
if ($_) {
$object = $_
}
if (! $object) {
throw 'must pass an -object parameter or pipe one in'
}
# load the required dll
$assembly = [System.Reflection.Assembly]::LoadWithPartialName($assemblyName)
# add each type as a member property
$assembly.GetTypes() |
where {$_.ispublic -and !$_.IsSubclassOf( [Exception] ) -and $_.name -notmatch "event"} |
foreach {
# avoid error messages in case it already exists
if (! ($object | get-member $_.name)) {
add-member noteproperty $_.name $_ -inputobject $object
}
}
}
And, to use it:
RICBERG470> $tfs | add-types "Microsoft.TeamFoundation.VersionControl.Client"
RICBERG470> $itemSpec = new-object $tfs.itemspec("$/foo", $tfs.RecursionType::none)
Basically what I do is crawl the assembly for nontrivial types, then write a "constructor" that uses Add-Member add them (in a structured way) to the objects I care about.
See also this followup post: http://richardberg.net/blog/?p=38
this is just a joke, joke...
$fullnames = New-Object ( [System.Collections.Generic.List``1].MakeGenericType( [String]) );
function using ( $name ) {
foreach ( $type in [Reflection.Assembly]::LoadWithPartialName($name).GetTypes() )
{
$fullnames.Add($type.fullname);
}
}
function new ( $name ) {
$fullname = $fullnames -like "*.$name";
return , (New-Object $fullname[0]);
}
using System.Windows.Forms
using FooCompany.Bar.Qux.Assembly.With.Ridiculous.Long.Namespace.I.Really.Mean.It
$a = new button
$b = new Thingamabob
Here's some code that works in PowerShell 2.0 to add type aliases. But the problem is that it is not scoped. With some extra work you could "un-import" the namespaces, but this should get you off to a good start.
##############################################################################
#.SYNOPSIS
# Add a type accelerator to the current session.
#
#.DESCRIPTION
# The Add-TypeAccelerator function allows you to add a simple type accelerator
# (like [regex]) for a longer type (like [System.Text.RegularExpressions.Regex]).
#
#.PARAMETER Name
# The short form accelerator should be just the name you want to use (without
# square brackets).
#
#.PARAMETER Type
# The type you want the accelerator to accelerate.
#
#.PARAMETER Force
# Overwrites any existing type alias.
#
#.EXAMPLE
# Add-TypeAccelerator List "System.Collections.Generic.List``1"
# $MyList = New-Object List[String]
##############################################################################
function Add-TypeAccelerator {
[CmdletBinding()]
param(
[Parameter(Position=1,Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
[String[]]$Name,
[Parameter(Position=2,Mandatory=$true,ValueFromPipeline=$true)]
[Type]$Type,
[Parameter()]
[Switch]$Force
)
process {
$TypeAccelerators = [Type]::GetType('System.Management.Automation.TypeAccelerators')
foreach ($a in $Name) {
if ( $TypeAccelerators::Get.ContainsKey($a) ) {
if ( $Force ) {
$TypeAccelerators::Remove($a) | Out-Null
$TypeAccelerators::Add($a,$Type)
}
elseif ( $Type -ne $TypeAccelerators::Get[$a] ) {
Write-Error "$a is already mapped to $($TypeAccelerators::Get[$a])"
}
}
else {
$TypeAccelerators::Add($a, $Type)
}
}
}
}
If you just need to create an instance of your type, you can store the name of the long namespace in a string:
$st = "System.Text"
$sb = New-Object "$st.StringBuilder"
It's not as powerful as the using directive in C#, but at least it's very easy to use.
Thanks everybody for your input. I've marked Richard Berg's contribution as an answer, because it most closely resembles what I'm looking for.
All your answers brought me on the track that seems most promising: In his blog post Keith Dahlby proposes a Get-Type commandlet that allows easy consutruction of types for generic methods.
I think there is no reason against exetending this to also search through a predefined path of assemblies for a type.
Disclaimer: I haven't built that -- yet ...
Here is how one could use it:
$path = (System.Collections.Generic, FooCompany.Bar.Qux.Assembly.With.Ridiculous.Long.Namespace.I.Really.Mean.It)
$type = get-type -Path $path List Thingamabob
$obj = new-object $type
$obj.GetType()
This would result in a nice generic List of Thingamabob. Of course I'd wrap up everthing sans the path definition in just another utility function. The extended get-type would include a step to resolve any given type agains the path.
#Requires -Version 5
using namespace System.Management.Automation.Host
#using module
I realize this is an old post, but I was looking for the same thing and came across this: http://weblogs.asp.net/adweigert/powershell-adding-the-using-statement
Edit: I suppose I should specify that it allows you to use the familiar syntax of...
using ($x = $y) { ... }

How do I write a PowerShell cmdlet to take either a HashTable or a PODO for input?

I have a powershell module that wraps around some web services. The web services take complex Plain Old Dot Net Objects (PODOs) and I have been using HashTables as in cmdlet parameters and New-Object MyPODO -Property $MyHashTable to transform the hashtable into the request object like so
function Get-Stuff ([HashTable]$WhatStuff) {
$service = New-ServiceProxy . . . .
$request = New-Object GetStuffRequest -Property $WhatStuff;
return $service.GetStuff($request);
$response;
}
However, sometimes I have a cmdlet whose response object can directly become a request object like so:
function Find-Stuff ([HashTable]$KindaStuff) {
$service = New-ServiceProxy . . . .
$request = New-Object GetStuffRequest -Property $KindaStuff;
return $service.SearchStuff($request);
}
Is there some sort of way to decorate the $WhatStuff parameter to accept either a HashTable or a PODO of a particular type?
James Tryand gave me this answer in a tweet.
The answer is to use Parameter Sets.
In one paramater set you accept a parameter of type HashTable, and in the other one, you accept the PODO type.
Maybe like below, depending on how you want to use it:
function Get-Stuff ($WhatStuff) {
if(($WhatStuff -isnot [HashTable]) -or ($WhatStuff -isnot [PODOType])){ throw "expect it to be Hashtable or object of type"}
...
}