How to provide options to script blocks? - powershell

I'm trying to learn about script blocks at https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_script_blocks?view=powershell-6.
I want to send a script block to an event registration with a command-line option to the registration command, like this:
$Block = {
Param($option)
Write-Host "The option was $option"
if ($option==ABC) {
Write-Host "ABC was specified"
}
}
But when I test this by appending
Invoke-Command -ScriptBlock $Block -option ABC
I get the error
Invoke-Command : A parameter cannot be found that matches parameter name 'option'.

When in doubt, read the documentation:
-ArgumentList
Supplies the values of local variables in the command. The variables in the command are replaced by these values before the command is run on the remote computer. Enter the values in a comma-separated list. Values are associated with variables in the order that they are listed. The alias for ArgumentList is "Args".
The values in ArgumentList can be actual values, such as "1024", or they can be references to local variables, such as "$max".
To use local variables in a command, use the following command format:
{param($<name1>[, $<name2>]...) <command-with-local-variables>} -ArgumentList <value> -or- <local-variable>
The "param" keyword lists the local variables that are used in the command. The ArgumentList parameter supplies the values of the variables, in the order that they are listed.
The first element from the -ArgumentList array becomes the value of the first named parameter in the scriptblock, the second array element becomes the value of the second named parameter, and so on.
Basically, your statement should look like this:
Invoke-Command -ScriptBlock $Block -ArgumentList 'ABC'
Also, the syntax of the comparison operation in your scriptblock is wrong. The equality comparison operator in PowerShell is -eq, not ==.

Remember that you can always check the documentation: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/invoke-command?view=powershell-6
$Block = {Param($option)
Write-Host "The option was $option"
if ($option -eq "ABC") // -eq instead of ==, string in quotations
{Write-Host "ABC was specified"}
}
Invoke-Command -ScriptBlock $Block -ArgumentList "ABC"
If there are more arguments you can list them in order. Using script blocks isn't something you want to do very often. It's better to create a function (https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions?view=powershell-6)

You can use the call operator on scriptblocks too, and just provide the arguments after a space or -option:
& $block abc
The option was abc
ABC was specified
And you can later assign the scriptblock to a function. That's what a function is.
$function:myfunc = $block

Related

Powershell Start-Job with command [duplicate]

I'd like to setup a cmdlet to start and stop mysql, and I'm trying to do so with Start-Job. the I've got the following in my Powershell profile:
$mysqlpath = "C:\Program Files\MySQL\MySQL Server 5.5\bin"
Function Start-Mysql
{
Start-Job -ScriptBlock { & "$mysqlpath\mysqld.exe" }
}
The variable doesn't seem to be expanding in the job command however? I must be missing some sort of scoping rule. Could someone please advise? Thanks!
you have to use the -argumentlist parameter see get-help start-job :
start-job -ScriptBlock { & $args[0] } -ArgumentList #($mysqlpath )
note that in V3 you just have to use the prefix using: before your varname ex:
Start-Job -ScriptBlock { & "$using:mysqlpath\mysqld.exe" }
Loïc MICHEL's answer is correct, but if you find it becomes difficult to deal with remembering which positional argument is which within the ScriptBlock, I'd like to offer a trick using the param keyword. Within the ScriptBlock, begin the body with param like you would for an advanced function, and put your code after also as if it were a function:
Note: The ScriptBlock param name does not need to be the same in the ScriptBlock and current session, it can be the same or something totally different. The important thing is you match the correct argument positionally in the -ArgumentList.
Start-Job { param( $mysqlpath ) & "$mysqlpath\mysqld.exe" } -ArgumentList $mysqlpath
This works because a ScriptBlock is just an unnamed function, so you can define parameters in mostly the same way you can when defining a proper function. The arguments in -ArgumentList are passed to the ScriptBlock as positional arguments in the order provided, so by default the order the arguments are passed is the same order they will be bound to named parameters in.
While the $using: scope is syntactically easier to work with, this method gets you the best of all worlds here, as the $using: scope cannot be used within the current session. This is incredibly useful you have a ScriptBlock that needs to be able to execute in any context and it's complicated enough that referencing the arguments by index becomes difficult to manage. This approach allows you to name your parameters and works with any ScriptBlock in all execution contexts, whether it's Start-Job, Invoke-Command, powershell.exe, or executing a ScriptBlock with the call operator &.
This is one of (if not the) most portable solution if you want to use named variables instead of referencing $args[i] for every variable.

start powershell wont accept variable as parameter

issue
the called powershell script will accept parameters but not all of them:
Current Set-Up and code:
I have a common folder where two .ps1 scripts are located:
DoWork.ps1
Workmanager.ps1
Workmanager.ps1 calls the Dowork.ps1:
$targetPath="M:\target"
echo "target path: $targetPath"
start powershell {.\DoWork.ps1 -target $targetPath -tempdrive D:\}
output (as expected):
target path: M:\target
DoWork.ps1 contains some start code:
param
(
[string]$tempdrive,
[string]$target,
[int] $threads = 8,
[int] $queuelength = -1
)
echo "variables:"
echo "temp drive: $tempdrive"
echo "target path: $target"
Unexpectedly, the $target is not beeing assigned. Previously I had the variable named $targetpath, which did not work either.
variables:
temp drive: D:\
target path:
Findings
It appears that the issue relies in Workmanager.ps1. Spcifying the parameter as fixed string rather than as variable will load the parameter. Any solution for this?
start powershell {.\DoWork.ps1 -target "foo" -tempdrive D:\}
When you use a ScriptBlock as an argument to powershell.exe, variables aren't going to be evaluated until after the new session starts. $targetPath has not been set in the child PowerShell process called by Workmanager.ps1 and so it has no value. This is actually an expected behavior of a ScriptBlock in general and behaves this way in other contexts too.
The solution is mentioned in the help text for powershell -?:
[-Command { - | <script-block> [-args <arg-array>] <========== THIS GUY
| <string> [<CommandParameters>] } ]
You must provide the -args parameter which will be passed to the ScriptBlock on execution (separate multiple arguments with a ,). Passed arguments are passed positionally, and must be referenced as though you were processing the arguments to a function manually using the $args array. For example:
$name = 'Bender'
& powershell { Write-Output "Hello, $($args[0])" } -args $name
However, especially with more complicated ScriptBlock bodies, having to remember which index of $args[i] contains the value you want at a given time is a pain in the butt. Luckily, we can use a little trick with defining parameters within the ScriptBlock to help:
$name = 'Bender'
& powershell { param($name) Write-Output "Hello, $name" } -args $name
This will print Hello, Bender as expected.
Some additional pointers:
The ScriptBlock can be multiline as though you were defining a function. way. The examples above are single line due to their simplicity.
A ScriptBlock is just an unnamed function, which is why defining parameters and referencing arguments within one works the same way.
To exemplify this behavior outside of powershell.exe -Command, Invoke-Command requires you to pass variables to its ScriptBlock in a similar fashion. Note however that answer uses an already-defined function body as the ScriptBlock (which is totally valid to do)
You don't need to use Start-Process here (start is its alias), at least as demonstrated in your example. You can simply use the call operator & unless you need to do something more complex than "run the program and wait for it to finish". See this answer of mine for more information.
If you opt to pass a string to powershell.exe instead, you don't need to provide arguments and your variables will get rendered in the current PowerShell process. However, so will any other unescaped variables that might be intended to set within the child process, so be careful with this approach. Personally, I prefer using ScriptBlock regardless, and just deal with the extra parameter definition and arguments.
Using the call & operator is optional when you are not executing a path rendered as a string. It can be omitted in the examples above, but is more useful like so:
& "C:\The\Program Path\Contains\spaces.exe"
& $programPathAsAVariable

Passing variables and object to start-job

I know that a script block executed via start-job cannot see the variables outside of the script block. To pass variables in you use the -arguments paramater. So why does this work (from MS article):
Start-Job -ScriptBlock { Get-Process -Name $args } -ArgumentList "powershell"
But this does not:
Start-Job -ScriptBlock { Get-aduser $args } -ArgumentList "samaccountname"
When I run it and receive the job I get the following error:
Cannot convert 'samaccountname' to the type 'Microsoft.ActiveDirectory.Management.ADUser' required by parameter 'Identity'. Specified method is not supported.
This same syntax works though outside of running it via start-job:
Get-aduser "samaccountname"
This last command is to demonstrate to you that the syntax is correct in the start-job script block. So why does the command expect an ADUser object when executed via start-job wheras outside of the script block it will accept a string?
I need to be able to execute the command via start-job
All credit here goes to Abraham in the comments above. He provided the answer but for whatever reason didn't submit it as an answer, so I am providing this to help others.
The issue is that -Arguments is always an array, even if you only provide one value. In my example where it didn't work with the second command (the get-Aduser one) is because I was passing an array to a paramater which expected a string. Changing the code to args[0] fixed this.
The first command worked because it's paramater accepts a string or an array.
NOTE: Passing complex objects to Start-job is a different story and has it's own issues, I posed this as a different question here

Passing arguments to Start-Job scriptblock?

I'd like to setup a cmdlet to start and stop mysql, and I'm trying to do so with Start-Job. the I've got the following in my Powershell profile:
$mysqlpath = "C:\Program Files\MySQL\MySQL Server 5.5\bin"
Function Start-Mysql
{
Start-Job -ScriptBlock { & "$mysqlpath\mysqld.exe" }
}
The variable doesn't seem to be expanding in the job command however? I must be missing some sort of scoping rule. Could someone please advise? Thanks!
you have to use the -argumentlist parameter see get-help start-job :
start-job -ScriptBlock { & $args[0] } -ArgumentList #($mysqlpath )
note that in V3 you just have to use the prefix using: before your varname ex:
Start-Job -ScriptBlock { & "$using:mysqlpath\mysqld.exe" }
Loïc MICHEL's answer is correct, but if you find it becomes difficult to deal with remembering which positional argument is which within the ScriptBlock, I'd like to offer a trick using the param keyword. Within the ScriptBlock, begin the body with param like you would for an advanced function, and put your code after also as if it were a function:
Note: The ScriptBlock param name does not need to be the same in the ScriptBlock and current session, it can be the same or something totally different. The important thing is you match the correct argument positionally in the -ArgumentList.
Start-Job { param( $mysqlpath ) & "$mysqlpath\mysqld.exe" } -ArgumentList $mysqlpath
This works because a ScriptBlock is just an unnamed function, so you can define parameters in mostly the same way you can when defining a proper function. The arguments in -ArgumentList are passed to the ScriptBlock as positional arguments in the order provided, so by default the order the arguments are passed is the same order they will be bound to named parameters in.
While the $using: scope is syntactically easier to work with, this method gets you the best of all worlds here, as the $using: scope cannot be used within the current session. This is incredibly useful you have a ScriptBlock that needs to be able to execute in any context and it's complicated enough that referencing the arguments by index becomes difficult to manage. This approach allows you to name your parameters and works with any ScriptBlock in all execution contexts, whether it's Start-Job, Invoke-Command, powershell.exe, or executing a ScriptBlock with the call operator &.
This is one of (if not the) most portable solution if you want to use named variables instead of referencing $args[i] for every variable.

accessing the $args array in powershell

I have a test powershell V2 script that looks like this:
function test_args()
{
Write-Host "here's arg 0: $args[0]"
Write-Host "here's arg 1: $args[1]"
}
test_args
If I call this from the powershell command prompt I get this on the screen:
here's arg[0]: [0]
here's arg[1]: [1]
Not quite what I wanted. It seems I have to copy $args[0] and $args[1] to new variables in the script before I can use them? If I do that I can access things fine.
Is there a way to access the indexed $args in my code? I've tried using curly braces around them in various ways but no luck.
I'll be moving to named parameters eventually, but the script I'm working on (not this demo one) is a straight port of a batch file.
Try this instead:
function test_args()
{
Write-Host "here's arg 0: $($args[0])"
Write-Host "here's arg 1: $($args[1])"
}
test_args foo bar
Note that it is $args and not $arg. Also when you use a PowerShell variable in a string, PowerShell only substitutes the variable's value. You can't directly use an expression like $args[0]. However, you can put the expression within a $() sub-expression group inside a double-quoted string to get PowerShell to evaluate the expression and then convert the result to a string.