Passing arguments to Start-Job scriptblock? - powershell

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.

Related

Dot-sourcing a self-elevate script

I have a self elevate snippet which is quite wordy, so I decided instead of duplicating it at the top of every script that needs to be run as admin to move it into a separate .ps1:
function Switch-ToAdmin {
# Self-elevate the script if required
if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator')) {
if ([int](Get-CimInstance -Class Win32_OperatingSystem | Select-Object -ExpandProperty BuildNumber) -ge 6000) {
$Cmd = #(
"-Command Set-Location `"$(Get-Location)`"; & `"$PSCommandPath`""
"-$($PSBoundParameters.Keys)"
)
$ProcArgs = #{
FilePath = 'PowerShell.exe'
Verb = 'RunAs'
ArgumentList = $Cmd
}
Start-Process #ProcArgs
Exit
}
}
}
So for every script that needs elevation I'd prepend
. "$PSScriptRoot\self-elevate.ps1"
Switch-ToAdmin
# rest of script
Doing above successfully procs the UAC prompt, but the rest of the script won't get executed.
Is this sorta stuff disallowed?
Darin and iRon have provided the crucial pointers:
Darin points out that the automatic $PSCommandPath variable variable in your Switch-ToAdmin function does not contain the full path of the script from which the function is called, but that of the script in which the function is defined, even if that script's definitions are loaded directly into the scope of your main script via ., the dot-sourcing operator.
The same applies analogously to the automatic $PSScriptRoot variable, which reflects the defining script's full directory path.
Also, more generally, the automatic $PSBoundParameters variable inside a function reflects that function's bound parameters, not its enclosing script's.
iRon points out that the Get-PSCallStack cmdlet can be used to get information about a script's callers, starting at index 1; the first object returned - index 0, when Get-PSCallStack output is captured in an array, represents the current command. Index 1 therefore refers to the immediate caller, which from the perspective of your dot-sourced script is your main script.
Therefore:
Replace $PSCommandPath with $MyInvocation.PSCommandPath, via the automatic $MyInvocation variable. $MyInvocation.PSCommandPath truly reflects the caller's full script path, irrespective of where the called function was defined.
Alternatively, use (Get-PSCallStack)[1].ScriptName, which despite what the property name suggests, returns the full path of the calling script too.
Replace $PSBoundParameters with (Get-PSCallStack)[1].InvocationInfo.BoundParameters
Note that there's also (Get-PSCallStack)[1].Arguments, but it seems to contain a single string only, containing a representation of all arguments that is only semi-structured and therefore doesn't allow robust reconstruction of the individual parameters.
As an aside:
Even if $PSBoundParameters contained the intended information, "-$($PSBoundParameters.Keys)" would only succeed in passing the bound parameters through if your script defines only one parameter, if that parameter is a [switch] parameter, and if it is actually passed in every invocation.
Passing arguments through robustly in this context is hard to do, and has inherent limitations - see this answer for a - complex - attempt to make it work as well as possible.

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

PowerShell - accessing script parameters, sent from Windows cmd shell

Questions
How can you access the parameters sent to PowerShell script (.ps1 file)?
Can you access parameters A: by name, B: by position, C: a mix of either?
Context
I recently tried to write a PowerShell script (.ps1) that would be called from a Windows batch file (.bat) (or potentially cmd shell, or AutoHotKey script) - which would pass parameters into the .ps1 script for it to use (to display a toast notification). Thanks to the instructions on ss64.com, I have used $args to do this kind of thing in the past, however for some reason I could access the parameters this way (despite passing parameters, $args[0] = '' (empty string) and $args.Count = 0) so eventually had to remove all the $args code, and replace it with Param() script instead.
I'm still not quite sure why, but thought this is something I should get to the bottom of before I try to write my next script...
Code Example 1: Args (un-named parameters)
ToastNotificationArgs.ps1
-------------------------
Write-Debug "The script has been passed $($args.Count) parameters"
If (!$args[0]) { # Handle first parameter missing }
If (!$args[1]) { # Handle second parameter missing }
Import-Module -Name BurntToast
New-BurntToastNotification -Text "$args[0], $args[1]"
^ I thought the above code was correct, but like I say, I kept struggling to access the parameters for some reason and could not figure out why. (If anyone can spot what I was doing wrong, please shout!)
Is $args[] a valid approach? I assume so given it's use of ss64.com, but maybe there are some pitfalls / limitations I need to be aware of?
Code Example 2: Param (named parameters)
ToastNotificationParams.ps1
---------------------------
Param(
[Parameter(Mandatory=$false, Position=0, ValueFromPipeline=$true)] [string]$Title,
[Parameter(Mandatory=$false, Position=1, ValueFromPipeline=$true)] [string]$Message
)
Import-Module -Name BurntToast
New-BurntToastNotification -Text "$Title, $Message"
^ This was the only way to get my script working in the end. However when I passed the parameters in, my calling cmd script sent the parameters by position i.e. (pwsh.exe -File "ToastNotificationParams.ps1" "This is the title" "Message goes here") rather than by named pairs. (Not sure if this is best practice, but is how my script was initially intended to be used to left it for now).
While Param() got my script working this time (and I also realise the inherent dangers of position-based parameters), there are times when a position-based approach might be necessary (e.g. the number of parameter is unknown)...
Code Example 3: Hybrid
ToastNotificationMix.ps1
------------------------
Param(
[Parameter(Mandatory=$false, Position=0, ValueFromPipeline=$true)] [string]$Title
)
Import-Module -Name BurntToast
For ( $i = 1; $i -lt $args.count; $i++ ) {
New-BurntToastNotification -Text "$Title, $args[i]"
}
Is something like this valid?.. If not (or there is a better solution), any help would be greatly appreciated!
Thanks in advance!
The automatic $args variable is only available in simple (non-advanced) functions / scripts. A script automatically becomes an advanced one by using the [CmdletBinding()] attribute and/or at least one per-parameter [Parameter()] attribute.
Using $args allows a function/script to accept an open-ended number of positional arguments, usually instead of, but also in addition to using explicitly declared parameters.
But it doesn't allow passing named arguments (arguments prefixed by a predeclared target parameter name, e.g., -Title)
For robustness, using an advanced (cmdlet-like) function or script is preferable; such functions / scripts:
They require declaring parameters explicitly.
They accept no arguments other than ones that bind to declared parameters.
However, you can define a single catch-all parameter that collects all positional arguments that don't bind to any of the other predeclared parameters, using [Parameter(ValueFromRemainingArguments)].
Explicitly defined parameters are positional by default, in the order in which they are declared inside the param(...) block.
You can turn off this default with [CmdletBinding(PositionalBinding=$false)],
which then allows you to selectively enable positional binding, using the Position property of the individual [Parameter()] attributes.
When you call a PowerShell script via the PowerShell's CLI's -File parameter, the invocation syntax is fundamentally the same as when calling script from inside PowerShell; that is, you can pass named arguments and/or - if supported - positional arguments.
Constraints:
The arguments are treated as literals.
Passing array arguments (,-separated elements) is not supported.
If you do need your arguments to be interpreted as they would be from inside PowerShell, use the -Command / -c CLI parameter instead
See this answer for guidance on when to use -File vs. `-Command.
To put it all together:
ToastNotificationMix.ps1:
[CmdletBinding(PositionalBinding=$false)]
Param(
[Parameter(Position=0)]
[string]$Title
,
[Parameter(Mandatory, ValueFromRemainingArguments)]
[string[]] $Rest
)
Import-Module -Name BurntToast
foreach ($restArg in $Rest) {
New-BurntToastNotification -Text "$Title, $restArg"
}
You can then call your script from cmd.exe as follows, for instance (I'm using pwsh.exe, the PowerShell (Core) CLI; for Windows PowerShell, use powershell.exe):
Positional binding only:
:: "foo" binds to $Title, "bar" to $Rest
pwsh -File ./ToastNotificationMix.ps1 foo bar
Mix of named and positional binding:
:: "bar" and "baz" both bind to $Rest
pwsh -File ./ToastNotificationMix.ps1 -Title foo bar baz