3

tl;dr

I want to find a Powershell version of the bash edit-and-execute-command widget or the zsh edit-command-line widget.

Background

Short commands get executed directly on the command-line, long complicated commands get executed from scripts. However, before they become "long", it helps to be able to test medium length commands on the command-line. To assist in this effort, editing the command in an external editor becomes very helpful. AFAIK Powershell does not support this natively as e.g. bash and zsh do.

My current attempt

I am new to Powershell, so I'm bound to make many mistakes, but I have come up with a working solution using the features of the [Microsoft.Powershell.PSConsoleReadLine] class. I am able to copy the current command-line to a file, edit the file, and then re-inject the edited version back into the command-line:

Set-PSReadLineKeyHandler -Chord "Alt+e" -ScriptBlock {
  $CurrentInput = $null

  # Copy current command-line input, save it to a file and clear it
  [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref] $CurrentInput, [ref] $null)
  Set-Content -Path "C:\Temp\ps_${PID}.txt" -Value "$CurrentInput"
  [Microsoft.PowerShell.PSConsoleReadLine]::KillRegion()

  # Edit the command with gvim
  Start-Job -Name EditCMD -ScriptBlock { gvim "C:\Temp\ps_${Using:PID}.txt" }
  Wait-Job  -Name EditCMD

  # Get command back from file the temporary file and insert it into the command-line
  $NewInput  = (Get-Content -Path "C:\Temp\ps_${PID}.txt") -join "`n"
  [Microsoft.PowerShell.PSConsoleReadLine]::Insert($NewInput)
}

Questions

My current solution feels clunky and a somewhat fragile. Are there other solutions? Can the current solution be improved?

Environment

  • OS Windows 10.0.19043.0
  • Powershell version 5.1.19041.1320
  • PSReadLine version 2.0.0

The solution I went with

Create a "baked" executable similar to what mklement0 showed. I prefer vim for this instead of `gvim, as it runs directly in the console:

'@vim -f %*' > psvim.cmd
$env:EDITOR = "psvim"
Set-PSReadLineKeyHandler -Chord "Alt+e" -Function ViEditVisually
Thor
  • 45,082
  • 11
  • 119
  • 130

2 Answers2

4

This code has some issues:

  • Temp path is hardcoded, it should use $env:temp or better yet [IO.Path]::GetTempPath() (for cross-platform compatibility).
  • After editing the line, it doesn't replace the whole line, only the text to the left of the cursor. As noted by mklement0, we can simply replace the existing buffer instead of erasing it, which fixes the problem.
  • When using the right parameters for the editor, it is not required to create a job to wait for it. For VSCode this is --wait (-w) and for gvim this is --nofork (-f), which prevents these processes to detach from the console process so the PowerShell code waits until the user has closed the editor.
  • The temporary file is not deleted after closing the editor.

Here is my attempt at fixing the code. I don't use gvim, so I tested it with VSCode code.exe. The code below contains a commented line for gvim too (confirmed working by the OP).

Set-PSReadLineKeyHandler -Chord "Alt+e" -ScriptBlock {
    $CurrentInput = $null

    # Copy current console line
    [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref] $CurrentInput, [ref] $null)

    # Save current console line to temp file
    $tempFilePath = Join-Path ([IO.Path]::GetTempPath()) "ps_$PID.ps1"
    Set-Content $tempFilePath -Value $CurrentInput -Encoding utf8

    # Edit the console line using VSCode
    code --new-window --wait $tempFilePath

    # Uncomment for using gvim editor instead
    # gvim -f $tempFilePath

    # The console doesn't like the CR character, so rejoin lines using LF only. 
    $editedInput = ((Get-Content -LiteralPath $tempFilePath) -join "`n").Trim()
    
    # Replace current console line with the content of the temp file
    [Microsoft.PowerShell.PSConsoleReadLine]::Replace(0, $currentInput.Length, $editedInput)

    Remove-Item $tempFilePath
}

Update:

This GitHub Gist includes an updated version of the code. New features including saving/restoring the cursor position, passing the cursor position to VSCode and showing a message while the console is blocked.

Notes:

  • A default installation of VSCode adds the directory of the VSCode binaries to $env:PATH, which enables us to write just code to launch the editor.
  • While UTF-8 is the default encoding for cmdlets like Set-Content on PowerShell Core, for Windows PowerShell the parameter -Encoding utf8 is required to correctly save commands that contain Unicode characters. For Get-Content specifying the encoding isn't necessary, because Windows PowerShell adds a BOM which Get-Content detects and PowerShell Core defaults to UTF-8 again.
  • To have Alt+E always available when you open a console, just add this code to your $profile file. Makes quick testing and editing of small code samples a breeze.
  • VSCode settings - for most streamlined experience, enable "Auto Save: onWindowChange". Allows you to close the editor and save the file (update the console line) with a single press to Ctrl+W.
zett42
  • 25,437
  • 3
  • 35
  • 72
4

tl;dr

  • PSReadLine comes with the feature you're looking for, namely the ViEditVisually function, and on Unix-like platforms it even has a default key binding.

  • For the feature to work, you need to set $env:VISUAL or $env:EDITOR to the name / path of your editor executable.

  • As of PSReadLine v2.1, if you also need to include options for your editor - such as -f for gvim - you need to create a helper executable that has the options "baked in".

    • Since creating a helper executable is cumbersome, a custom emulation of the feature, as you have attempted and as refined in zett42's helpful answer, may be the simpler solution for now. zett42's answer additionally links to a Gist that improves the original functionality by preserving cursor positions.

    • GitHub issue #3214 suggests adding support for recognizing executables plus their options in these environment variables.

Details below.


  • The PSReadLine module ships with such a feature, namely the ViEditVisually function.

  • Its default key bindings, if any, depend on PSReadLine's edit mode, which you can set with Set-PSReadLineOption -EditMode <mode>:

    • Windows mode (default on Windows): not bound
    • Emacs mode (default on macOS and Linux): Ctrl-xCtrl-e
    • Vi mode: v in command mode (press Esc to enter it)
  • Use Set-PSReadLineKeyHandler to establish a custom key binding, analogous to the approach in the question; e.g., to bind Alt-e:

    • Set-PSReadLineKeyHandler -Chord Alt+e -Function ViEditVisually
  • For the function to work, you must define the editor executable to use, via either of the following environment variables, in order of precedence: $env:VISUAL or $env:EDITOR; if no (valid) editor is defined, a warning beep is emitted and no action is taken when the function is invoked.

    • E.g., to use the nano editor on macOS: $env:VISUAL = 'nano'

    • The value must refer to an executable file - either by full path or, more typically, by name only, in which case it must be located in a directory listed in $env:PATH

      • Note: .ps1 scripts do not qualify as executable files, but batch files on Windows and shebang-line-based shell scripts on Unix-like platforms do.
    • As of PSReadLine 2.1, including options for the executable - such as --newindow --wait for code (Visual Studio Code) is not supported.

      • Therefore, for now, if your editor of choice requires options, you need to create a helper executable that has these options "baked in"; see examples below.

      • GitHub issue #3214 proposed adding support for allowing to specify the editor executable plus options as the environment-variable value, which is something that Git (which recognizes the same variables) already supports; for instance, you could then define:
        $env:VISUAL = 'code --new-window --wait'


Example configuration with a helper executable for code (Visual Studio Code):

  • Create a helper executable in the user's home directory in this example:

    • On Windows:

      '@code --new-window --wait %*' > "$HOME\codewait.cmd"
      
    • On Unix-like platforms:

      "#!/bin/sh`ncode --new-window --wait `"$@`"" > "$HOME/codewait"; chmod a+x "$HOME/codewait"
      
  • Add a definition of $env:VISUAL pointing to the helper executable to your $PROFILE file, along with defining a custom key binding, if desired:

$env:VISUAL = "$HOME/codewait"

# Custom key binding
Set-PSReadLineKeyHandler -Chord Alt+e -Function ViEditVisually
mklement0
  • 382,024
  • 64
  • 607
  • 775