Search code examples
powershellsshcmdpowershell-core

Open pwsh with command from cmd without exiting


I am trying to start a Powershell window that starts an ssh session with the following command:

pwsh.exe -noexit -Command {ssh <username>@<host>}

This works when executed from a pwsh.exe window itself: enter image description here but when executed from cmd, the run window (Win+R) or Task Scheduler (what I need it for), it just displays the command that should have been executed and starts pwsh: enter image description here

That is, the command that doesn't work outside of PowerShell is:

pwsh.exe -noexit -Command {ssh username@host}

The commands have of course been tested with actual websites where, again, 1 works and 2 doesn't.
Any help would be greatly appreciated! Thank you!


Solution

  • Note:

    • The answer below applies to the CLIs of both PowerShell editions, (powershell.exe for Windows PowerShell, pwsh for PowerShell (Core) 7+)

    • powershell -Command "& { ... }" / pwsh -Command "& { ... }" is an anti-pattern, and therefore to be avoided:

      • Use "...", not "& { ... }", i.e. specify all commands directly,[1] which avoids unnecessary syntactic ceremony and unnecessary processing (though the latter won't matter in practice).

      • This anti-pattern, which is unfortunately very common, presumably arose because older versions of the CLI documentation erroneously suggested that & { ... } is required, but this has since been corrected.


    tl;dr

    • From inside PowerShell, use pwsh { ... } (-Command implied); in your case:

      pwsh.exe -noexit { ssh username@host }
      
      • However, note that it's rarely necessary to call the PowerShell CLI from inside PowerShell (which involves creating a child process). Here you could simply call ssh username@host directly.
    • From outside PowerShell, use pwsh -Command " ... " (or pwsh -c " ..." ); in your case (as in your own answer):

      pwsh.exe -noexit "ssh username@host"
      
      • Outside can mean:

        • Calling from another shell, notably cmd.exe
          • Calling from another shell means that its syntax requirements must be satisfied first, which can get tricky with " chars. that are escaped as \" - see this answer for robust workarounds.
        • Calling from a no-shell context such as the Windows Run dialog (WinKey+R) and commands launched by Task Scheduler.
        • All (native) outside contexts on Windows require double-quoting ("..."), but if you're calling from a Unix (POSIX-compatible) shell such as Bash, single-quoting is also an option.

    For a comprehensive overview of the PowerShell CLI, see this post.


    Background information:

    { ... } is the literal form of a PowerShell script block, loosely speaking a reusable piece of code that must be invoked on demand, either with & (in a child scope) or with . (directly in the caller's scope):

    • Only inside PowerShell can it be used with the PowerShell CLI to pass commands to execute, because PowerShell there offers { ... } as syntactic sugar:

      • Behind the scenes, the code inside the script block is Base64-encoded and passed on the command line that is ultimately constructed via -EncodedCommand; similarly, any arguments passed to -Args are Base64-encoded and passed via -EncodedArguments, and CLIXML (PowerShell's XML-based inter-process serialization format) is requested as the output format via -OutputFormat XML
      • This, in conjunction with automatic deserialization of the output received, ensures that the benefits of PowerShell's rich (.NET-based) type system and multiple output streams are preserves as much as possible (type fidelity has inherent limitations when serialization is involved - see this answer).
    • When the PowerShell CLI is called from the outside (or even via string from inside PowerShell), the syntactic sugar does not apply, and a script-block literal (invariably provided via a string argument) is parsed as just that: a piece of code to be called later.

      • Thus, if you try something like the following:

          # !! WRONG - simply *prints* what's between { and }
          # (Also applies if you don't use "..." from outside PowerShell.)
          pwsh -c "{ Get-Date }"
        
        • PowerShell constructs a script block, and, since it is not assigned to a variable, outputs it by default, which means a string representation of the (otherwise unused) script block prints, which is the verbatim content of the script block, sans { and }.
        • That is, verbatim  Get-Date  is printed, which you can also verify as follows from inside PowerShell: { Get-Date }.ToString()

    [1] Technically, you could use "& { ... } ..." if you wanted to pass arguments from inside your command string to the embedded script block, though that level of encapsulation will rarely be necessary in practice; a contrived example (call from outside PowerShell):
    pwsh -c "& { 'Time is now: ' + $args[0].TimeOfDay } (Get-Date)"