Home > Net >  Alternative to $MyInvocation
Alternative to $MyInvocation

Time:02-12

When deploying PowerShell scripts from my RMM (NinjaOne), the scripts are called from a .bat (batch file).

Example:

@powershell -ExecutionPolicy Bypass -File "test.ps1" -stringParam "testing" -switchParam > "output.txt" 2>&1

The script I am calling requires PowerShell 7 , so I need to restart the script by calling pwsh with the current parameters. I planned to accomplish this via the following:

Invoke-Command { & pwsh -Command $MyInvocation.Line } -NoNewScope

Unfortunately, $MyInvocation.Line does not return the correct result when a PowerShell script is called from a batch file. What alternatives exist that would work in this scenario?

Notes:

  • I am unable to make changes to the .bat file.
  • $PSBoundParameters also does not return the expected result.

Edit: I've discovered the reason for $MyInvocation / $PSBoundParameters not being set properly is due to the use of -File instead of -Command when my RMM provider calls the PowerShell script from the .bat file. I've suggested they implement this change to resolve the issue. Until they do, I am still looking for alternatives.

CodePudding user response:

I'd try putting a file called powershell.bat early on your path (at least, in a directory earlier on the path than the C:\Windows\System32\WindowsPowerShell\v1.0\; entry) and assemble the appropriate parameters (I've no idea of your required structure for $myinvocationline - no doubt it could be derived from the parameters delivered to powershell.bat).

My thinking is that this should override powershell, re-assemble the bits and deliver them to pwsh.

CodePudding user response:

I put this line in test.ps1:

$MyInvocation | Format-List -Property *

Found this content in output.txt:



MyCommand             : test.ps1
BoundParameters       : {}
UnboundArguments      : {-stringParam, testing, -switchParam}
ScriptLineNumber      : 0
OffsetInLine          : 0
HistoryId             : 1
ScriptName            : 
Line                  : 
PositionMessage       : 
PSScriptRoot          : 
PSCommandPath         : 
InvocationName        : D:\Temp\StackOverflow\71087897\test.ps1
PipelineLength        : 2
PipelinePosition      : 1
ExpectingInput        : False
CommandOrigin         : Runspace
DisplayScriptPosition : 



Then tried this in test.ps1:

[string]$MySelf = $MyInvocation.InvocationName
Write-Host "###$MySelf###"
[string[]]$Params = $MyInvocation.UnboundArguments
foreach ($Param in $Params) {
    Write-Host "Param: '$Param'"
}

And found this in output.txt:

###D:\Temp\StackOverflow\71087897\test.ps1###
Param: '-stringParam'
Param: 'testing'
Param: '-switchParam'

Then tried this in test.ps1:

[string]$Line = "$($MyInvocation.InvocationName) $($MyInvocation.UnboundArguments)"
Write-Host "$Line"

And found this in output.txt:

D:\Temp\StackOverflow\71087897\test.ps1 -stringParam testing -switchParam

Does this get you to where you need to be?

CodePudding user response:

Leaving RMM providers out of the picture (whose involvement may or may not matter), the following test.ps1 content should work:

# Note: Place [CmdletBinding()] above param(...) to make
#       the script an *advanced* one, which then prevents passing
#       arguments that don't bind to declared parameters.
param(
  [string] $stringParam,
  [switch] $switchParam
)

# If invoked via powershell.exe, re-invoke via pwsh.exe
if ((Get-Process -Id $PID).Name -eq 'powershell') {
   # $PSCommandPath is the current script's full file path,
   # and @PSBoundParameters uses splatting to pass all bound arguments through.
   pwsh -ExecutionPolicy Bypass -File $PSCommandPath @PSBoundParameters
   exit $LASTEXITCODE
}

# Getting here means that the file is being executed by pwsh.exe

# Print the bound parameters.
$PSBoundParameters

exit 0

The above assumes that only arguments for formally declared parameters are passed; if you do want to support extra arguments, append @args to the pwsh -ExecutionPolicy Bypass -File $PSCommandPath @PSBoundParameters line above.
Alternatively, as suggested in the code comments, place [CmdletBinding()] above the param(...) block, which makes the script an advanced one, in which case passing extra arguments is actively prevented (and in which case $args isn't defined).


As for what you tried:

  • $MyInvocation.Line isn't defined when a script is called via PowerShell's CLI; however, the automatic $PSCommandPath variable, reflecting the running script's full file path, is defined.

    • Even if $MyInvocation.Line were defined, it wouldn't enable robust pass-through of the original arguments, due to potential quoting issues and - when called from inside PowerShell - due to reflecting unexpanded arguments.
  • Also, it is virtually pointless to use Invoke-Command for local invocations - see this answer.

  • Related