2

I'm looking for method to create a wrapper around Invoke-Command that restores the current directory that I'm using on the remote machine before invoking my command. Here's what I tried to do:

    function nice_invoke {
        param(
            [string]$Computer,
            [scriptblock]$ScriptBlock
        )
    
        Set-PSDebug -Trace 0

        $cwd = (Get-Location).Path
        write-host "cmd: $cwd"
    
        $wrapper = {
            $target = $using:cwd
            if (-not (Test-Path "$target")) {
                write-host "ERROR: Directory doesn't exist on remote"
                exit 1
            }
            else {
                Set-Location $target
            }
            $sb = $using:ScriptBlock
    
            $sb.Invoke() | out-host
       }
    
       # Execute Command on remote computer in Same Directory as Local Machine
       Invoke-Command -Computer pv3039 -ScriptBlock $wrapper    
    }

Command Line:

    PS> nice_invoke -Computer pv3039 -ScriptBlock {get-location |out-host; get-ChildItem | out-host }

Error Message:

Method invocation failed because [System.String] 
does not contain a method named 'Invoke'.
+ CategoryInfo          : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : MethodNotFound
+ PSComputerName        : pv3039
2
  • If I'm not mistaken the using shouldbe unecessary to prepare the second ScriptBlock. Did you investigate the datatype of $sb within the function? As you can see by the error $sb is only a string rather than a scripblock which is why it does not have a invoke option. In place it could be possible to just use $ScriptBlock. Commented Oct 27, 2021 at 17:09
  • @Seth unfortunately that is not the case, the ScriptBlock contents are internally a string-literal That is, variable expansion won't occur until the ScriptBlock is prepared for execution via .Invoke() or when using the call operator &. Either the $using: scope or providing an -ArgumentList is required here, but neither will preserve the original ScriptBlock type. Commented Oct 27, 2021 at 17:26

2 Answers 2

3

You can't pass a ScriptBlock like this with the $using: scope, it will get rendered to a string-literal first. Use the [ScriptBlock]::Create(string) method instead within your $wrapper block to create a ScriptBlock from a String:

$sb = [ScriptBlock]::Create($using:ScriptBlock)
$sb.Invoke() | Out-Host

Alternatively, you could also use Invoke-Command -ArgumentList $ScriptBlock, but you still have the same issue with the ScriptBlock getting rendered as a string. Nonetheless, here is an example for this case as well:

# Call `Invoke-Command -ArgumentList $ScriptBlock`
# $args[0] is the first argument passed into the `Invoke-Command` block
$sb = [ScriptBlock]::Create($args[0])
$sb.Invoke() | Out-Host

Note: While I kept the format here in the way you were attempting to run the ScriptBlock in your original code, the idiomatic way to run ScriptBlocks locally (from the perspective the nested ScriptBlock it is a local execution on the remote machine) is to use the Call Operator like & $sb rather than using $sb.Invoke().

With either approach, the nested ScriptBlock will execute for you from the nested block now. This limitation is similar to how some other types are incompatible with shipping across remote connections or will not survive serialization with Export/Import-CliXml; it is simply a limitation of the ScriptBlock type.


Worthy to note, this limitation persists whether using Invoke-Command or another cmdlet that initiates execution via a child PowerShell session such as Start-Job. So the solution will be the same either way.

Sign up to request clarification or add additional context in comments.

4 Comments

@SantiagoSquarzon That won't work because $ScriptBlock does not exist in the remote session. The $using: scope or -ArgumentList $ScriptBlock is required (the latter would require referencing $args[0] instead of $ScriptBlock in that case).
You're right, my bad sorry, I was having a really hard time understanding OP's code or even why is the function useful at all.
@SantiagoSquarzon Yeah a better approach would be to simply cd $using:pwd rather than create $cwd and do it that way. However, there is still value in understanding how to provide a nested ScriptBlock to a remote or parallel PowerShell session.
Nicely done, though I suggest at least mentioning that a more "natural" way to invoke script blocks is with & (or .). That script blocks deserialize as strings is certainly surprising and has been reported in GitHub issue #11698; unfortunately, the decision was made not to fix the problem, for security reasons I personally find unconvincing.
-2
function nice_invoke {
    param(
        [string]$Computer,
        [scriptblock]$ScriptBlock
    )
    Set-PSDebug -Trace 0    
    $cwd = (Get-Location).Path
    write-host "cmd: $cwd"  
    $wrapper = {
        $target = $using:cwd
        if (-not (Test-Path "$target")) {
            write-host "ERROR: Directory doesn't exist on remote"
            exit 1
        }
        else {
            Set-Location $using:cwd
        }       
        $sb = [scriptblock]::Create($using:ScriptBlock)     
        $sb.Invoke() 
    }   
    # Execute Command on remote computer in Same Directory as Local Machine
    Invoke-Command -Computer pv3039 -ScriptBlock $wrapper 
}
nice_invoke -Computer pv3039 -ScriptBlock {
      hostname
      get-location 
      #dir
}

1 Comment

Code-only answers are generally considered low-quality on Stack Overflow. You may want to provide an explanation as to what changes you made to the code and why.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.