none
Powershell: Remote Invoke-Command with -ScriptBlock changes types of arguments if scripblock contains variables RRS feed

  • Question

  • What's going on here???

    PS C:\> Invoke-Command -ComputerName somemachine -Credential $cred -ArgumentList {Write-Host hi} -ScriptBlock {param($sb); Write-Host $sb.GetType().Name} 
    ScriptBlock 
    PS C:\> Invoke-Command -ComputerName somemachine -Credential $cred -ArgumentList {Write-Host hi} -ScriptBlock {param($sb); Write-Host $sb.GetType().Name; $a=3} 
    String 
    PS C:\> Invoke-Command -ComputerName somemachine -Credential $cred -ArgumentList {Get-ChildItem} -ScriptBlock {param($sb); Write-Host $sb.GetType().Name} 
    ScriptBlock 
    PS C:\> Invoke-Command -ComputerName somemachine -Credential $cred -ArgumentList {Get-ChildItem} -ScriptBlock {param($sb); Write-Host $sb.GetType().Name; $a=3} 
    String

    Friday, January 11, 2019 3:42 PM

All replies

  • Incorrect use of arguments.  You cannot use a scriptblock argument that is a scriptblock.

    To pass a script block as an argument you have to "Invoke" it inside the Invoke-Command script block like this:

    $sb = {
        param ($sb)
        $sb.Invoke()
    }
    Invoke-Command -ScriptBlock $sb -ArgumentList { Write-Host hi }
    Invoke-Command -ScriptBlock $sb -ArgumentList { Get-ChildItem c:\ }


    \_(ツ)_/

    Friday, January 11, 2019 6:33 PM
  • Thanks for your reply! The question was not really about invoking a nested scriptblock, it's about the observed change in the type of $sb (from ScriptBlock to String} when the seemingly unrelated change of adding "$a=3" is made. Here are the first two examples formatted (sorry, should have done that in the first place, didn't realise you could!). Note that I am already passing the "nested" scriptblock as an argument via the -ArgumentList parameter:

    PS C:\> Invoke-Command -ComputerName somemachine -Credential $cred -ArgumentList {Write-Host hi} -ScriptBlock {param($sb); Write-Host $sb.GetType().Name} 
    ScriptBlock 
    PS C:\> Invoke-Command -ComputerName somemachine -Credential $cred -ArgumentList {Write-Host hi} -ScriptBlock {param($sb); Write-Host $sb.GetType().Name; $a=3} 
    String 
    

    The 3rd and 4th examples were intended to show that this isn't actually specific to passing a scriptblock to the scriptblock. Here's what they should show:

    PS C:\> Invoke-Command -ComputerName somemachine -Credential $cred -ArgumentList $(Get-ChildItem) -ScriptBlock {param($cis); Write-Host $cis.GetType().Name} 
    DirectoryInfo 
    PS C:\> Invoke-Command -ComputerName somemachine -Credential $cred -ArgumentList $(Get-ChildItem) -ScriptBlock {param($cis); Write-Host $cis.GetType().Name; $a=3} 
    PSObject
    See that again the only thing that has changed is that I've added "$a = 3" to the scriptblock, but somehow that has caused the type of $cis to change from DirectoryInfo to PSObject.

    Friday, January 11, 2019 7:47 PM
  • Just a simple coding error.  If you learn to format and structure your code correctly you would be able to see your syntax and coding errors;

    $sb = {
        param ($sb)
        Write-Host ($sb.GetType()).Name
        $a = 3
    }
    Invoke-Command -ArgumentList { Write-Host hi } -ScriptBlock $sb

    By doing it this way all bits are visible and easy to see.

    Without the extra parens the argument to Write-Host is parsed as a string.

    Here is how to test a script block:

    PS D:\scripts> $sb = {
    >>     param ($sb)
    >>     Write-Host ($sb.GetType()).Name
    >>     $a = 3
    >> }
    PS D:\scripts> $sb.Invoke('Hello')
    String
    PS D:\scripts> $sb.Invoke({})
    ScriptBlock
    PS D:\scripts>

    This separates it from other more complex code which can help you to see the forest for the trees.

    \_(ツ)_/

    Friday, January 11, 2019 8:15 PM
  • I also see that you are using all kinds of odd approaches without any specific reason.  Start by analyzing what the code does and try to see how your syntactical approach might be fooling you.


    \_(ツ)_/

    Friday, January 11, 2019 8:16 PM
  • Hi, thanks again! I greatly appreciate your efforts to help. In case it helps to set the level, I am an experienced professional software developer.

    Taking your example:

    $sb = {
        param ($sb)
        $sb.Invoke()
    }
    
    Invoke-Command -ScriptBlock $sb -ArgumentList { Write-Host hi }
    

    When run locally, this of course prints "hi".

    We will make one change to it, which is to invoke the command *remotely*, as it was in my example and as mentioned in the title of the original post (if it isn't run remotely the strange behaviour does not occur):

    $sb = {
        param ($sb)
        $sb.Invoke()
        $a = 3
    }
    
    Invoke-Command -ComputerName $somecomputer -Credential $cred -ScriptBlock $sb -ArgumentList { Write-Host hi }
    

    Note that the *only* change here from your example is that I have added the "-ComputerName" and "-Credential" parameters. You will of course need to set the variables $somecomputer and $cred appropriately.

    You might expect the above to print "hi". I certainly would. It doesn't, on my machine at least. What I get is:

    Method invocation failed because [System.String] does not contain a method named 'Invoke'.

    That is, the type of the object that has been passed in to the scriptblock as the parameter named $sb is String, not ScriptBlock, and the script does not work because String has no method called Invoke().

    Interestingly, in this case the type of $sb is String even if there is no "$a = 3" line. In my original one-liner example, the parameter comes through as a String if I include that line and as a ScriptBlock if I don't. Whether the parameter comes through as a ScriptBlock or a String seems to change depending on irrelevant external factors, and I've even seen a case where the variable appears at some points inside the scriptblock to be a ScriptBlock and at other points to be a String.

    I suspect this might be a bug in the PowerShell engine (though I don't know the language well enough to be sure, hence this question), or if it is not it is certainly an extremely strange behaviour that I would like to understand so that I can write programs reliably in this language. This is the reason for my trying seemingly "odd approaches" - I'm trying to demonstrate a strange behaviour in the language, not perform a task. I figured out a way to do what I was actually trying to do ages ago.

    Apologies for my original post, which was unhelpfully terse. I hope the above makes the nature of the problem clear. This may not be the correct forum to ask this (it's the third one I've tried after github and uservoice), in which case I'd love to know the right one!

    In case it helps in reproducing this, my powershell version info iis:

      PS C:\stuff\tmp> $PSVersionTable.PSVersion                                                                                              Major  Minor  Build  Revision                                                                                                                      -----  -----  -----  --------                                                                                                                            5      1      16299  820

    Friday, January 11, 2019 10:53 PM
  • Ok. I see what you are asking.  Code does not "marshall" remotely.  A code block will get marshaled to a string.

    Change the scriptblock to this:

    $sb = {
        param
    ($sb)
        Write-Host $sb

    }

    It will print out the string value of the SB.  This is by design and affects all languages and systems.


    \_(ツ)_/

    Friday, January 11, 2019 11:36 PM
  • That I can certainly accept, and it's how I figured it worked. However, if you look carefully at my first example it is explicitly showing that the type received is ScriptBlock in the first case and String in the second:

    PS C:\> Invoke-Command -ComputerName somemachine -Credential $cred -ArgumentList {Write-Host hi} -ScriptBlock {param($sb); Write-Host $sb.GetType().Name} 
    ScriptBlock 
    PS C:\> Invoke-Command -ComputerName somemachine -Credential $cred -ArgumentList {Write-Host hi} -ScriptBlock {param($sb); Write-Host $sb.GetType().Name; $a=3} 
    String 

    From the point of view of GetType(), it seems to behave as if it's receiving something that claims to be a ScriptBlock but which magically turns into a String if you look at it funny or make a loud noise.

    I suspect the weirdness might be limited to the return value of GetType() on the object received. I think the object always actually behaves to all intents and purposes as a String but just somehow GetType() spuriously returns ScriptBlock instead of String under certain circumstances. That's certainly a lot less troubling than having it sometimes marshalled as a real ScriptBlock and sometimes not, which is what I thought was happening. Pretty odd, though.

    Saturday, January 12, 2019 12:19 AM
  • But using a better structure for the script block doesn't do that.

    These two report the scriptblock correctly.

    Invoke-Command -ComputerName ws701 -ArgumentList {Write-Host hi} -ScriptBlock {param($sb)  Write-Host ($sb.GetType()).Name;$i = ""}

    The parens force correct evaluation in the write-host.  THe following also evaluates correctly.

    Invoke-Command -ComputerName ws701 -ArgumentList {Write-Host hi} -ScriptBlock {param($sb)  $sb.GetType().Name;$i = ""}

    Without the "string" parameter interfering we get the correct type of the script block when passed remotely.

    This also shows how it works.

    Invoke-Command -ComputerName ws701 -ArgumentList {Write-Host hi} -ScriptBlock {param($sb)  $sb;$i = ""}

    I do suspect that the parser could be a little friendlier but what it is doing is correct for the syntaxes being used.


    \_(ツ)_/


    • Edited by jrv Saturday, January 12, 2019 12:32 AM
    Saturday, January 12, 2019 12:31 AM