i made this script in powershell, but i'm not doing something correctly (I'm trying to learn self-taught uwu)
This is the script in question.
function Get-DesiredProcess {
$DesiredProcess=Read-Host "Welcome to de advanced task manager! Choose an action to do: 1. WUP (WindowsUpdate) 2. MDP (ProxyModification)"
Switch ($DesiredProcess)
{
WUP {$ChosenProcess=Install-WUpdate}
MDP {$ChosenProcess=Set-Proxy}
}
If ($DesiredProcess -eq $null) {
Write-Error "You must specify an action!"
return Get-DesiredProcess
}
Else {
Set-Variable -Name "DesiredProcess" -Value "ChosenProcess"
}
}
Get-DesiredProcess
Write-Output $DesiredProcess
Write-Output $ChosenProcess
The 2 last "Write-Output" are just for testing if it did registry the variables correctly or not (spoiler, it didn't)
When the Read-Host have not been answered, it should output "You specify an action!" but does nothing:
Welcome to de advanced task manager! Choose an action to do: 1. WUP (WindowsUpdate) 2. MDP (ProxyModification):
PS C:\Users\user1\Documents\scriptsfiles>
And when a choice is made, it should keep the variable with this part:
Set-Variable -Name "DesiredProcess" -Value "ChosenProcess"
Instead of that it executes directly the choosen process...
Thanks in advance!
If you want your function to set a new value to a variable defined outside the function, use scoping inside the function, so
$ChosenProcess = 'Install-WUpdate' --> $script:ChosenProcess = 'Install-WUpdate'.
However, easier to understand is to have your function output something the rest of the code can deal with. In this case, since you want both what the user typed in, AND what the chosen process for that input would be, I would suggest having the function output an object that has both properties like:
function Get-DesiredProcess {
# inside the function both variables have LOCAL scope
$ChosenProcess = $null # initialize to nothing, or an empty string
$DesiredProcess = Read-Host "Welcome to de advanced task manager! Choose an action to do: 1. WUP (WindowsUpdate) 2. MDP (ProxyModification)"
switch ($DesiredProcess) {
'WUP' {$ChosenProcess = 'Install-WUpdate'}
'MDP' {$ChosenProcess = 'Set-Proxy'}
default {
Write-Warning "You did not specify an accepted action. Type 'WUP' or 'MDP'"
Get-DesiredProcess
}
}
# only output if there is a matching chosen process
if (![string]::IsNullOrWhiteSpace($ChosenProcess)) {
# now have the function return both what the user typed and what the chosen action should be
[PsCustomObject]#{
DesiredProcess = $DesiredProcess
ChosenProcess = $ChosenProcess
}
}
}
$result = Get-DesiredProcess
Using this would output something like
Welcome to de advanced task manager! Choose an action to do: 1. WUP (WindowsUpdate) 2. MDP (ProxyModification): 7
WARNING: You did not specify an accepted action. Type 'WUP' or 'MDP'
Welcome to de advanced task manager! Choose an action to do: 1. WUP (WindowsUpdate) 2. MDP (ProxyModification): wup
DesiredProcess ChosenProcess
-------------- -------------
wup Install-WUpdate
Now, the following code can simply act on what is in variable $result.ChosenProcess
What I understand after reading your description, you need to declare all of the variables as global otherwise you need to access those variables inside of the function. A function variable can not be accessible from outside of the function. As per my understanding from your description, your code will be like this:
function Get-DesiredProcess
{
$DesiredProcess=Read-Host "Welcome to de advanced task manager! Choose an action to do: 1. WUP (WindowsUpdate) 2. MDP (ProxyModification)"
switch ($DesiredProcess)
{
1 {$ChosenProcess="Install-WUpdate"}
2 {$ChosenProcess="Set-Proxy"}
}
If ($DesiredProcess -eq [string]::empty)
{
Write-Error "You must specify an action!"
Get-DesiredProcess
}
Else
{
Set-Variable -Name "DesiredProcessName" -Value "$ChosenProcess"
Write-host "You chose this: "$DesiredProcessName
}
}
Get-DesiredProcess
Related
I need some help with a basic PowerShell script that sends a 200 HTTP request to a website to check if it's online. I have a script that checks one website, but I need to update it in order to check multiple websites every time it runs. I need to check 8-10 websites at a time.
Thanks.
Here is my script so far:
$HTTP_Request = [System.Net.WebRequest]::Create('http://google.com')
$HTTP_Response = $HTTP_Request.GetResponse()
$HTTP_Status = [int]$HTTP_Response.StatusCode
If ($HTTP_Status -eq 200) {
Write-Host "Site is OK!"
}
Else {
Write-Host "The Site may be down, please check!"
}
If ($HTTP_Response -eq $null) { }
Else { $HTTP_Response.Close() }
I tried a few things. Added another HTTP_Request line, added another set of parentheses with the URL. I'm a complete novice when it comes to PowerShell, so any assistance would be greatly appreciated.
My approach would be to create an array of addresses and then loop through them. I'm not great with powershell syntax but something like...
$websites = #("www.google.com", "www.facebook.com", "www.amazon.com", "www.microsoft.com")
foreach ($site in $websites) {
Test-Connection $site -Count 1 -Quiet
if ($?) {
Write-Host "$site is up"
} else {
Write-Host "$site is down"
}
}
I have a few scripts that create multiple instances of PSDrive to remote instances. I want to make certain that each instance of PSDrive created is cleaned up.
I have a Powershell module like the following. This is a simplified version of what I actually run:
function Connect-PSDrive {
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
$Root,
[String]
$Name = [Guid]::NewGuid().ToString(),
[ValidateSet("Registry","Alias","Environment","FileSystem","Function","Variable","Certificate","WSMan")]
[String]
$PSProvider = "FileSystem",
[Switch]
$Persist = $false,
[System.Management.Automation.PSCredential]
$Credential
)
$parameters = #{
Root = $Root;
Name = $Name;
PSProvider = $PSProvider;
Persist = $Persist;
}
$drive = $script:drives | Where-Object {
($_.Name -eq $Name) -or ($_.Root -eq $Root)
}
if (!$drive) {
if ($Credential) {
$parameters.Add("Credential", $Credential)
}
$script:drives += #(New-PSDrive #parameters)
if (Get-PSDrive | Where-Object { $_.Name -eq $Name }) {
Write-Host "The drive '$Name' was created successfully."
}
}
}
function Disconnect-PSDrives {
[CmdletBinding()]
param ()
$script:drives | Remove-PSDrive -Force
}
Each time I invoke the function Connect-PSDrive, I can see that a new drive is successfully created and a reference is added to $script:drives. At the end of the calling script, I have a finally block that invokes Disconnect-PSDrives and this fails with the following exception.
Remove-PSDrive : Cannot find drive. A drive with the name 'mydrive' does not exist.
At C:\git\ops\release-scripts\PSModules\PSDriveWrapper\PSDriveWrapper.psm1:132 char:22
+ $script:drives | Remove-PSDrive -Force
+ ~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (mydrive:String) [Remove-PSDrive], DriveNotFoundException
+ FullyQualifiedErrorId : DriveNotFound,Microsoft.PowerShell.Commands.RemovePSDriveCommand
I want to know why references to the PSDrive objects I created are available in $script:drives, and yet Remove-PSDrive fails to locate the objects.
I also want to know how I can manage these PSDrive instances without needing to return each instance to the calling script such that Disconnect-PSDrives works.
A few extra notes:
I'm creating these drives with the Persist flag as false.
Running these multiple times errors with too many multiple connections being made to a machine. This is why I think that connections are not being cleaned up. If my assumption is wrong, please kindly explain why connections are cleaned up.
I am a little surprised that it cannot remove from the object reference; but I assume that your issue is with scope. PSDrives are local scope by default so when your function exits, they are no longer visible. Use the -Scope parameter for New-PSDrive and you will likely be successful. (As a side note: during Disconnect-PSDrives you will likely want to clear the list in case of multiple calls.)
That being said, you should never need to clean up the PSDrives like you are doing. Likely the reason you are experiencing too many connections is, once again, a scoping issue (that is, they still exist but you no longer see them). Try running it multiple times where you close PowerShell and start a new instance each time--you will no longer see too many connections. Why? Because PowerShell cleans up all non-persistent drives at the end of your session. You do not need to clean up the drives between sessions/instances; and within an session/instance (assuming you have proper scoping) you can re-use the drives so there is no need to create duplicates; ergo, you should never really need this functionality. That being said, I might assume you have some niche use case for this?
Starting premise: very restrictive environment, Windows 7 SP1, Powershell 3.0. Limited or no possibility of using external libraries.
I'm trying to re-write a bash tool I created previously, this time using PowerShell. In bash I implemented autocompletion to make the tool more user friendly and I want to do the same thing for the PowerShell version.
The bash version worked like this:
./launcher <Tab> => ./launcher test (or dev, prod, etc.)
./launcher test <Tab> => ./launcher test app1 (or app2, app3, etc.)
./launcher test app1 <Tab> => ./launcher test app1 command1 (or command2, command3, etc.).
As you can see, everything was dynamic. The list of environments was dynamic, the list of application was dynamic, depending on the environment selected, the list of commands was also dynamic.
The problem is with the test → application connection. I want to show the correct application based on the environment already selected by the user.
Using PowerShell's DynamicParam I can get a dynamic list of environments based on a folder listing. I can't however (or at least I haven't found out how to) do another folder listing but this time using a variable based on the existing user selection.
Current code:
function ParameterCompletion {
$RuntimeParameterDictionary = New-Object Management.Automation.RuntimeDefinedParameterDictionary
# Block 1.
$AttributeCollection = New-Object Collections.ObjectModel.Collection[System.Attribute]
$ParameterName = "Environment1"
$ParameterAttribute = New-Object Management.Automation.ParameterAttribute
$ParameterAttribute.Mandatory = $true
$ParameterAttribute.Position = 1
$AttributeCollection.Add($ParameterAttribute)
# End of block 1.
$parameterValues = $(Get-ChildItem -Path ".\configurations" -Directory | Select-Object -ExpandProperty Name)
$ValidateSetAttribute = New-Object Management.Automation.ValidateSetAttribute($parameterValues)
$AttributeCollection.Add($ValidateSetAttribute)
$RuntimeParameter = New-Object Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)
$RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
# Block 2: same thing as in block 1 just with 2 at the end of variables.
# Problem section: how can I change this line to include ".\configurations\${myVar}"?
# And what's the magic incantation to fill $myVar with the info I need?
$parameterValues2 = $(Get-ChildItem -Path ".\configurations" -Directory | Select-Object -ExpandProperty Name)
$ValidateSetAttribute2 = New-Object Management.Automation.ValidateSetAttribute($parameterValues2)
$AttributeCollection2.Add($ValidateSetAttribute2)
$RuntimeParameter2 = New-Object
Management.Automation.RuntimeDefinedParameter($ParameterName2, [string], $AttributeCollection2)
$RuntimeParameterDictionary.Add($ParameterName2, $RuntimeParameter2)
return $RuntimeParameterDictionary
}
function App {
[CmdletBinding()]
Param()
DynamicParam {
return ParameterCompletion "Environment1"
}
Begin {
$Environment = $PsBoundParameters["Environment1"]
}
Process {
}
}
I would recommend using argument completers, which are semi-exposed in PowerShell 3 and 4, and fully exposed in version 5.0 and higher. For v3 and v4, the underlying functionality is there, but you have to override the TabExpansion2 built-in function to use them. That's OK for your own session, but it's generally frowned upon to distribute tools that do that to other people's sessions (imagine if everyone tried to override that function). A PowerShell team member has a module that does this for you called TabExpansionPlusPlus. I know I said overriding TabExpansion2 was bad, but it's OK if this module does it :)
When I needed to support versions 3 and 4, I would distribute my commands in modules, and have the modules check for the existence of the 'Register-ArgumentCompleter' command, which is a cmdlet in v5+ and is a function if you have the TE++ module. If the module found it, it would register any completer(s), and if it didn't, it would notify the user that argument completion wouldn't work unless they got the TabExpansionPlusPlus module.
Assuming you have the TE++ module or PSv5+, I think this should get you on the right track:
function launcher {
[CmdletBinding()]
param(
[string] $Environment1,
[string] $Environment2,
[string] $Environment3
)
$PSBoundParameters
}
1..3 | ForEach-Object {
Register-ArgumentCompleter -CommandName launcher -ParameterName "Environment${_}" -ScriptBlock {
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
$PathParts = $fakeBoundParameter.Keys | where { $_ -like 'Environment*' } | sort | ForEach-Object {
$fakeBoundParameter[$_]
}
Get-ChildItem -Path ".\configurations\$($PathParts -join '\')" -Directory -ErrorAction SilentlyContinue | select -ExpandProperty Name | where { $_ -like "${wordToComplete}*" } | ForEach-Object {
New-Object System.Management.Automation.CompletionResult (
$_,
$_,
'ParameterValue',
$_
)
}
}
}
For this to work, your current working directory will need a 'configurations' directory contained in it, and you'll need at least three levels of subdirectories (reading through your example, it looked like you were going to enumerate a directory, and you would go deeper into that structure as parameters were added). The enumerating of the directory isn't very smart right now, and you can fool it pretty easy if you just skip a parameter, e.g., launcher -Environment3 <TAB> would try to give you completions for the first sub directory.
This works if you will always have three parameters available. If you need a variable # of parameters, you could still use completers, but it might get a little trickier.
The biggest downside would be that you'd still have to validate the users' input since completers are basically just suggestions, and users don't have to use those suggestions.
If you want to use dynamic parameters, it gets pretty crazy. There may be a better way, but I've never been able to see the value of dynamic parameters at the commandline without using reflection, and at that point you're using functionality that could change at the next release (the members usually aren't public for a reason). It's tempting to try to use $MyInvocation inside the DynamicParam {} block, but it's not populated at the time the user is typing the command into the commandline, and it only shows one line of the command anyway without using reflection.
The below was tested on PowerShell 5.1, so I can't guarantee that any other version has these exact same class members (it's based off of something I first saw Garrett Serack do). Like the previous example, it depends on a .\configurations folder in the current working directory (if there isn't one, you won't see any -Environment parameters).
function badlauncher {
[CmdletBinding()]
param()
DynamicParam {
#region Get the arguments
# In it's current form, this will ignore parameter names, e.g., '-ParameterName ParameterValue' would ignore '-ParameterName',
# and only 'ParameterValue' would be in $UnboundArgs
$BindingFlags = [System.Reflection.BindingFlags] 'Instance, NonPublic, Public'
$Context = $PSCmdlet.GetType().GetProperty('Context', $BindingFlags).GetValue($PSCmdlet)
$CurrentCommandProcessor = $Context.GetType().GetProperty('CurrentCommandProcessor', $BindingFlags).GetValue($Context)
$ParameterBinder = $CurrentCommandProcessor.GetType().GetProperty('CmdletParameterBinderController', $BindingFlags).GetValue($CurrentCommandProcessor)
$UnboundArgs = #($ParameterBinder.GetType().GetProperty('UnboundArguments', $BindingFlags).GetValue($ParameterBinder) | where { $_ } | ForEach-Object {
try {
if (-not $_.GetType().GetProperty('ParameterNameSpecified', $BindingFlags).GetValue($_)) {
$_.GetType().GetProperty('ArgumentValue', $BindingFlags).GetValue($_)
}
}
catch {
# Don't do anything??
}
})
#endregion
$ParamDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
# Create an Environment parameter for each argument specified, plus one extra as long as there
# are valid subfolders under .\configurations
for ($i = 0; $i -le $UnboundArgs.Count; $i++) {
$ParameterName = "Environment$($i + 1)"
$ParamAttributes = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$ParamAttributes.Add((New-Object Parameter))
$ParamAttributes[0].Position = $i
# Build the path that will be enumerated based on previous arguments
$PathSb = New-Object System.Text.StringBuilder
$PathSb.Append('.\configurations\') | Out-Null
for ($j = 0; $j -lt $i; $j++) {
$PathSb.AppendFormat('{0}\', $UnboundArgs[$j]) | Out-Null
}
$ValidParameterValues = Get-ChildItem -Path $PathSb.ToString() -Directory -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name
if ($ValidParameterValues) {
$ParamAttributes.Add((New-Object ValidateSet $ValidParameterValues))
$ParamDictionary[$ParameterName] = New-Object System.Management.Automation.RuntimeDefinedParameter (
$ParameterName,
[string[]],
$ParamAttributes
)
}
}
return $ParamDictionary
}
process {
$PSBoundParameters
}
}
The cool thing about this one is that it can keep going as long as there are folders, and it automatically does parameter validation. Of course, you're breaking the laws of .NET by using reflection to get at all those private members, so I would consider this a terrible and fragile solution, no matter how fun it was to come up with.
I'm trying to understand how .GetNewClosure() works within the context of a script cmdlet in PowerShell 2.
In essence I have a function that returns an object like so:
function Get-AnObject {
param(
[CmdletBinding()]
[Parameter(....)]
[String[]]$Id
..
[ValidateSet('Option1','Option2')]
[String[]]$Options
)
...
$T = New-Object PSCustomObject -Property #{ ..... }
$T | Add-Member -MemberType ScriptProperty -Name ExpensiveScriptProperty -Value {
$this | Get-ExpensiveStuff
}.GetNewClosure()
..
}
Providing I do not have the validate set options the closure appears to work fine. If it is included however the new closure fails with the following error.
Exception calling "GetNewClosure" with "0" argument(s): "Attribute cannot be added because it would cause the variable Options with value to become invalid."
Presumably the closure is trying to capture the context of the call to the Cmdlet. Since the parameter "Options" is not bound at all this is not nicely with the parameter validation.
I imagine it's possible to avoid this by placing validation as code within the body of the Cmdlet instead of making use of the [Validate*()] decorators -- but this seems nasty and quite obscure. Is there a way of fusing these two ideas?
The "Attribute cannot be added" message is (or was) a PowerShell bug, I've submitted it to Microsoft with this bug report. That particular issue seems to have been fixed, (perhaps around V5.1. but anyone interested in Powershell Closures may still find info below interesting.
There is a workaround which works in earlier versions, but first here's a simplified repro case that produces the same error:
function Test-ClosureWithValidation {
[CmdletBinding()]
param(
[Parameter()]
[ValidateSet('Option1','Option2')]
[String[]]$Options
)
[scriptblock] $closure = {"OK"}.GetNewClosure();
$closure.Invoke()
}
Test-ClosureWithValidation -Options Option1
The workaround depends on the fact that GetNewClosure() works by iterating over the local variables in the calling script's context, binding these local variables into the script's context. The bug occurs because its copying the $Options variable including the validation attribute. You can work around the bug by creating a new context with only the local variables you need. In the simple repro above, it is a one-line workaround:
[scriptblock] $closure = &{ {"OK"}.GetNewClosure();}
The line above now creates a scope with no local variables. That may be too simple for your case; If you need some values from the outer scope, you can just copy them into local variables in the new scope, e.g:
[scriptblock] $closure = &{
$options = $options;
{"OK $options"}.GetNewClosure();
}
Note that the second line above creates a new $options variable, assigning it the value of the outer variable, the attributes don't propagate.
Finally, I'm not sure in your example why you need to call GetNewClosure at all. The variable $this isn't a normal local variable, it will be available in your script property whether or not you create a closure. Example:
function Test-ScriptPropertyWithoutClosure {
[CmdletBinding()]
param(
[Parameter()]
[ValidateSet('Option1','Option2')]
[String[]]$Options
)
[pscustomobject]#{ Timestamp= Get-Date} |
Add-Member ScriptProperty ExpensiveScriptProperty {
$this | get-member -MemberType Properties| % Name
} -PassThru
}
Test-ScriptPropertyWithoutClosure -Options Option1 | fl
I believe this might work:
function Get-AnObject {
param(
[CmdletBinding()]
[Parameter(....)]
[String[]]$Id
..
[ValidateSet('Option1','Option2')]
[String[]]$Options
)
...
$sb = [scriptblock]::create('$this | Get-ExpensiveStuff')
$T = New-Object PSCustomObject -Property #{ ..... }
$T | Add-Member -MemberType ScriptProperty -Name ExpensiveScriptProperty -Value $sb
.. }
That delays creation of the script block until run time.
I'm setting a handler for the InstantMessageReceived event, but it only seems to fire on outgoing text messages, not incoming. Here is the code I'm running:
# Register the app with Growl
$icon = "https://docs.google.com/uc?export=download&id=0B1Weg9ZlwneOZmY2b1NSVXJ0Q2s"
$types = '"new-im","new-call","invitation","share"'
& 'C:\Program Files (x86)\Growl for Windows\growlnotify.exe' /a:Lync /ai:$icon /r:$types "Registration."
#We just need the Model API for this example
import-module "C:\Program Files (x86)\Microsoft Lync\SDK\Assemblies\Desktop\Microsoft.Lync.Model.Dll"
#Get a reference to the Client object
$client = [Microsoft.Lync.Model.LyncClient]::GetClient()
#Set the client to reference to the local client
$self = $client.Self
# What do we do here?
$conversationMgr = $client.ConversationManager
# Register events for existing conversations.
$i = 0
for ($i=0; $i -lt $conversationMgr.Conversations.Count; $i++) {
Register-ObjectEvent -InputObject $conversationMgr.Conversations[$i].Modalities[1] -EventName "InstantMessageReceived" `
-SourceIdentifier "new im $i" `
-action {
$message = $EventArgs.Text
Write-Host "DEBUG: New incoming IM - $message"
# Try to get the name of the person...
$contactInfo = $Event.Sender.Conversation.Participants[1].Contact.GetContactInformation([Microsoft.Lync.Model.ContactInformationType[]] #("FirstName", "LastName", "DisplayName", "PrimaryEmailAddress", "Photo", "IconUrl", "IconStream"))
$name = " "
if ($contactInfo.Get_Item("FirstName")) { $name = $contactInfo.Get_Item("FirstName") + " " + $contactInfo.Get_Item("LastName") + ":" }
elseif ($contactInfo.Get_Item("DisplayName")) { $name = $contactInfo.Get_Item("DisplayName") + ":"}
else { $name = $contactInfo.Get_Item("PrimaryEmailAddress") + ":" }
# We need to check if the Lync window (conversation?) has focus or not.
if (1) {
# We need to send our growl notification.
& 'C:\Program Files (x86)\Growl for Windows\growlnotify.exe' /a:Lync /n:new-im /t:"New Instant Message" "$name $message"
}
}
}
# If this exits, no more events.
while (1) { }
Every time I type out an IM message to someone else, it does what I'm trying to do for incoming messages. But nothing ever fires for those, just outgoing. I've been through all the documentation, and there aren't any other candidate events, I'm sure it's this one. But the Modality object just stores some stuff about whether it's an IM or screensharing and the like, nothing useful.
http://msdn.microsoft.com/en-us/library/lync/microsoft.lync.model.conversation.instantmessagemodality_di_3_uc_ocs14mreflyncclnt_members(v=office.14).aspx
Where am I screwing up on this? I prefer answers in Powershell, but I don't think this is a problem specific to Powershell, so if you know how to do it in C# or Visual Basic or something like that, I'd appreciate that too.
I don't have Lync so I can test this myself, but take a look at this link where it shows how to use the API.
The problem is(from what I understand) that there is a modality per participant per media. So for a conversation with two members using only text, there will be 2 modalities, one for incoming messages(from the remote participant) and one for outgoing. This is specified here in
Occurs when an instant message is received, or sent if the InstantMessageModality belongs to the local participant.
Source: MSDN
When you register your object-event, you register it to "your modality", and not the remote modality. To fix it it seems to you need to take each conversation from the manager, look at each participant except the one representing you (check the IsSelf property). Then take the modality from the participants(except yourself) and register for the InstantMessageReceived event.
At least that's what I got out of it, but as said I have no experience with Lync so I could easily be wrong.
My guess at how it could be done(VERY untested):
# What do we do here? You get the manager the keeps track of every conversation
$conversationMgr = $client.ConversationManager
# Register events for existing conversations.
#You may need to use '$conversation in $conversationMgr.GetEnumerator()'
foreach ($conversation in $conversationMgr) {
#Get remote participants
$conversation.Participants | where { !$_.IsSelf } | foreach {
#Get IM modality
$textmod = [InstantMessageModality]($_.Modalities[ModalityTypes.InstantMessage])
Register-ObjectEvent -InputObject $textmod -EventName "InstantMessageReceived" `
-SourceIdentifier "new im $i" `
-action {
#...
}
}
}