Search code examples
powershellpowershell-core

Specific Alternative for Invoke-Expression when calling starship


I am using starship to configure my prompt in Powershell.

The starship documentation supplies this code that is now added to Profile, so the propt is available on startup of PS:

Invoke-Expression (&starship init powershell)

Now I am playing with module PSScriptAnalyzer and it is Warning me not to use Invoke-Expression.

On further reading I see that Invoke-Expression is a potential security risk.

Is it a security risk in the context of how I'm using it? If so then is there a specific alternative I can use in my Profile?

I've tried using & starship init powershell in my Profile without success, as this is written to the host on start-up:

Invoke-Expression (& '/usr/local/bin/starship' init powershell --print-full-init | Out-String)


Solution

  • Is it a security risk in the context of how I'm using it?

    Whether it is a security risk ultimately doesn't depend on whether or not you use Invoke-Expression, but only on whether you trust the code being executed - and it's fair to assume that you do trust the code that comes with Starship.

    While Invoke-Expression (iex) is generally to be avoided, the case at hand is a legitimate use:

    & starship init powershell outputs PowerShell source code, which - in order to take effect for prompt customization - must be dot-sourced, i.e. executed directly in the caller's scope (rather than in a child scope).

    Passing code to Invoke-Expression implicitly dot-sources it, and is the simplest way to do so for code that isn't already saved in a file (*.ps1); in the latter case, you would use . , the dot-sourcing operator.

    The equivalent - but more verbose - approach that avoids Invoke-Expression is to combine [scriptblock]::Create() with the . operator (which, like &, the call operator, can also act on script blocks):

    # Parse the PowerShell source code output by `starship init powershell`
    # into a script block...
    $scriptBlock = [scriptblock]::Create((& starship init powershell))
    # ... and execute it dot-sourced
    . $scriptBlock
    

    As an aside, re why Invoke-Expression is also used in the code output by starship init powershell, as shown in your question, effectively resulting in a nested Invoke-Expression invocation:

    This indirection is used in order to produce single-line output from starship init, given that passing multiple strings to Invoke-Expression as an argument would break the call; e.g.:

    # !! FAILS, because only a *single* string (though possibly multiline) 
    # !! may be passed.
    Invoke-Expression ('1+2', '3+4')
    
    # OK, via the pipeline: -> 3, 7
    '1+2', '3+4' | Invoke-Expression
    
    # OK, as a *single* *multiline* string
    Invoke-Expression "1+2`n3+4"
    

    The reason that multiline output from an external program turns into multiple strings is that PowerShell relays the lines in a program's output as a stream of separate strings, which become a string array when captured or passed as an argument; e.g.:

    # Passes each output line (with trailing newline remove)
    # separately through the pipeline:
    # -> '[one]', '[two]'
    sh -c 'echo one; echo two' | ForEach-Object { "[$_]" }
    

    On Windows, use cmd /c 'echo one& echo two' as input.

    As shown above, passing multiple strings via the pipeline to Invoke-Expression does work, so that the initialization code could be simplified as follows:

    starship init powershell --print-full-init | Invoke-Expression
    

    Even when using an argument the initialization could be simplified, by directly incorporating the Out-String call:

    Invoke-Expression (starship init powershell --print-full-init | Out-String)
    

    If maximizing performance is the goal (though I doubt it will be noticeable in practice), use the -join operator to join the output lines with newlines ("`n"):

    Invoke-Expression ((starship init powershell --print-full-init) -join "`n")