Closed jimbobmcgee closed 3 weeks ago
I suggest this would be a PSReadLine feature.
There's a really cool module called psedit
that lets you get a fullblown editor in your terminal. You could set up a custom keyhandler in PSReadLine which would get the current input buffer, save it to a temp file, open that file with psedit, and replace the input buffer with the file content.
As for this:
I have to throw my hands up in the air, bang the whole thing into the clipboard (using the mouse of all things) and paste it into an editor.
You can press Ctrl+A to select all and Ctrl+C to copy it all. There's no need to use a mouse and deal with the line continuation characters.
You can use script block to edit command
PS > & {
edit your command inside this unnamed function
}
This is already implemented in PSReadLine. By default if your edit mode is Vi
it will be v
in command mode. You can also set it in your profile:
Set-PSReadLineKeyHandler -Chord F8 -Function ViEditVisually
It uses $env:EDITOR
to determine what application to launch.
@SeeminglyScience That's good enough for me. I don't use Vi mode, but adding the handler seems to work without being in any specific mode.
Now, if only it could get a binding by default, so I didn't have to add it across every server I have to use.
@jimbobmcgee that would be needed to be raised as a feature request in the PSReadline repo but is a good candidate for inclusion in a profile script that you can access on any/all servers you work on
Let me provide some additional context for the ViEditVisually
PSReadLine function:
tl;dr:
The bottom section of this comment contains a custom implementation of ViEditVisually
that improves on the latter in several ways and is easy to customize.
The following section discusses ViEditVisually
current limitations.
ViEditVisually
checks $env:VISUAL
first, then $env:EDITOR
(in-session changes are honored).
In the absence of both variables, or in the event that the effective value doesn't refer to the name or path of an existing executable, invocation fails and merely results in a beep, with no additional feedback. (If $env:VISUAL
is found to be invalid, $env:EDITOR
is not checked as a fallback.) Notably, you cannot include options for the target editor in the value - see below.
Arguably, providing both a default and warning in the event of an invalid value would be preferable: https://github.com/PowerShell/PSReadLine/issues/4004
The default key bindings are:
(Get-PSReadLineOption).EditMode
/ Set-PSReadLineOption -EditMode $newMode
, with $newMode
being one of Windows
, Emacs
, Vi
):Windows
: none:Emacs
: Ctrl-x, Ctrl-eVi
: Esc, vWindows
): none:Emacs
): Ctrl-x, Ctrl-e$PROFILE
file, as previously shown and noted. Limitations and behavioral notes:
A fundamental limitation as of this writing is that the $env:VISUAL
/ $env:EDITOR
value may be a mere executable name or path only; that is, you cannot "bake in" options; for instance $env:VISUAL='code'
works fine, but $env:VISUAL='code --new-window --wait'
does not - and the latter options are the prerequisite for making code
(the Visual Studio Code CLI) suitable for use with ViEditVisually
. (The function fundamentally makes no attempt to invoke GUI editors synchronously, so that $env:VISUAL = 'Notepad'
also doesn't work, for instance; in short: you need a console-based CLI that synchronously invokes a dedicated editor for the file path it is passed.)
Potentially removing this limitation is the subject of https://github.com/PowerShell/PSReadLine/issues/3214, not least because git
, for instance, which respects the same environment variables does have support for including options.
A workaround is to define a helper batch file / shell script that wraps the editor call with the desired options, and point $env:VISUAL
/ $env:EDITOR
to it.
Another fundamental limitation as of this writing is that $env:VISUAL
/ $env:EDITOR
must refer to an external (native) program, which precludes use of cmdlet-based console editors such as the aforementioned psedit
aka Show-PSEditor
; the latter is notable for its support for syntax highlighting and error analysis, tab-completion and reformatting.
Due to lack of a unified mechanism in external editors for controlling the cursor position on startup, the cursor will be at the start of the edit buffer, irrespective where the cursor was on the command line at the time of invocation of ViEditVisually
.
ViEditVisually
passes the current command-line buffer via a temporary file('s path) to the configured editor, and uses the potentially modified content of that file as the result.
The upshot is that you must ensure that the temporary file is saved before or in the context of exiting the editor, which usually requires an explicit additional step, unless the editor is configured to auto-save.
Again, there's no unified mechanism for such modal editing interactions, but the aforementioned micro
editor offers a superior mechanism: when its stdout stream is redirected, it modifies its behavior to not require saving, and to output the buffer contents (which may originally have been received via stdin) via stdout.
Unlike the equivalent Bash readline function, which automatically submits the command "returned" from the editor, ViEditVisually
merely replaces the current command-line buffer with it, requiring Enter to submit it.
To overcome these limitations, a custom implementation is required (which too can be placed in $PROFILE
), such as the following, which builds on @zett42's helpful Gist:
It uses key binding Alt+e for invocation, which is cross-platform-friendly; adjust as needed.
If neither $env:VISUAL
nor $env:EDITOR
is defined, it uses code
(Visual Studio Code) by default, if present, with additional fallbacks, including micro
and several others; adjust as needed.
It supports specifying PowerShell commands via $env:VISUAL
nor $env:EDITOR
, so that Show-PSEditor
may be used, for instance, though the latter appears to be rough around the edges as of this writing (see below).
It knows how to invoke code
and micro
with the appropriate options, which includes positioning the cursor in the editor's buffer in the same place as in the command-line buffer on invocation.
It auto-submits the edited buffer, if saved (if not saved, no action is taken, i.e. the original command-line buffer is retained and nothing is executed). To change that, simply comment out the two [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()
lines.
code
, for Show-PSEditor
, and - as an exception among terminal-based editors, micro
.nano
cannot be invoked from a -ScriptBlock
argument, because PSReadLine seemingly redirects (captures) stdout. In that event, the standard ViEditVisually
is invoked instead, and - due to lack of knowledge whether or not the buffer was modified - the result is not auto-submitted; also, the command-line cursor position cannot be preserved in that case.$VerbosePreference = 'Continue'
to see behind-the-scenes details about the invocation.In terms of UX:
code
provides a rich PowerShell experience, but invoking it, which involves loading the PowerShell extension, is slow, and the fact that a different application is activated is visually disruptive. Also, on macOS a workaround is needed to re-activate the calling terminal. Explicit saving of the temporary file is needed, though the Files: Auto Save
setting may be set to onFocusChange
to make closing the window with Ctrl+W sufficient; however, note that auto-saving is then performed invariably and that the setting applies to all future windows, which may not be desired.
micro
, as a terminal-based editor, minimizes the visual disruption and loads quickly, and is invoked in a manner that (invariably) auto-saves the result when pressing Ctrl+Q; while this makes for the most seamless integration overall, micro
lacks PowerShell-specific features other than syntax highlighting.
Show-PSEditor
has great potential, but as of this writing is plagued by several bugs and one notable usability issue: you must explicitly save with Ctrl+S; pressing just Ctrl+Q quietly discards the changes. Also, Ctrl+S doesn't seem to work in Windows Terminal. Finally, it lacks support for positioning the cursor.
# Custom implementation of the ViEditVisually PSReadLine function.
Set-PSReadLineKeyHandler -Chord 'Alt+e' -ScriptBlock {
# To support cross-edition use of potentially undefined variables such as $IsMacOS
Set-StrictMode -Off
# Get current buffer text.
$bufferText = $null; $cursorPos = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref] $bufferText, [ref] $cursorPos)
# Translate the current cursor position on the command line into a line-column pair, to pass
# to those editors that support positioning the cursor via their CLI.
$lineNo = 1; $precedingNewLinePos = -1
foreach ($m in [regex]::Matches($bufferText, '\n')) {
if ($m.Index -lt $cursorPos) { ++$lineNo; $precedingNewLinePos = $m.Index }
else { break }
}
$colNo = $cursorPos - $precedingNewLinePos
# Determine the target editor.
$editor = foreach ($cmd in $env:VISUAL, $env:EDITOR) { if ($cmd) { $cmd; break } }
$options = @()
if ($configuredViaEnvVar = $editor) { # $env:VISUAL or $env:EDITOR is set.
$orgVal = $editor
if (-not ($editor = Get-Command -ErrorAction Ignore $editor)) {
# $editor not being a command means one of two things:
# * The editor binary path / command name doesn't exist.
# * The env. var. value is an editor binary (path) *plus options*.
# Check for the latter:
# NOTE: Hypothetically, the following command expands "$"-prefixed tokens embedded in the value of $editor.
# While this could be considered code injection, the very fact of allowing specification of a *binary itself*, even
# by *literal* path - via $env:VISUAL or $env:EDITOR - is a security vulnerability, so this case is not worth guarding against.
$editor, [array] $options = Invoke-Expression "Write-Output -- $orgVal"
if (-not ($editor = Get-Command -ErrorAction Ignore $editor)) { throw "Not a valid editor binary name / path / command or command line: «$orgVal»" }
}
# Translate alias 'psedit' into the name of its target cmdlet.
if ($editor -eq 'psedit') { $editor = 'Show-PSEditor' }
}
else {
# No explicitly defined editor - look for candidates in order of preference.
# Note: So as to also support `psedit` (`Show-PSEditor`), we do not limit the command
# lookup to -Type Application
$editor = Get-Command -ErrorAction Ignore 'code', 'micro', 'gedit', 'nano', 'vi', 'vim', 'emacs' | Select-Object -First 1
if (-not $editor) {
throw 'No suitable modal text editor found.'
}
}
if ($editor.ResolvedCommand) { $editor = $editor.ResolvedCommand }
# Get the mere editor name, without path and filename extension.
# Note: For cmdlets, this reports the cmdlet name.
$editorBaseName = [System.IO.Path]::GetFileNameWithoutExtension($editor)
# Determine editor characteristics.
$needTempFile = $editorBaseName -ne 'micro' # Only `micro` supports modal editing via stdin and stdout.
# *Assume* that editors other than well-known terminal-based editors are GUI editors.
# Note that we cannot know for sure (hypothetically, on Windows only, the binary could be examined for whether it is a console-subsystem application).
# !! If a custom editor specified via $env:VISUAL / $env:EDITOR results in the terminal *freezing* on invocation, add its binary base name to this list.
$isGuiEditor = $editorBaseName -notin 'Show-PSEditor', 'micro', 'mcedit', 'nano', 'vi', 'vim', 'emacs'
# !! If the target editor is (a) terminal-based and (b) doesn't work when its stdout is redirected, which is MOST of them,
# !! we must delegate to the ViEditVisually PSReadLine function.
# !! Note that redirecting stdout cannot be avoided, because PSReadLine *captures* the output from this -ScriptBlock argument.
# !! Trying to force output to a terminal with `>/dev/tty` on Unix does NOT work, and on Windows in PS Core only half-works with `>\\.\CON`.
# !! Redirecting to ViEditVisually PSReadLine function implies:
# !! * NO support for passing the cursor position.
# !! * We cannot auto-submit the returned buffer, becase we won't know if it was modified.
# !! The only terminal-based editor NOT affected by a stdout redirection is `micro`.
# !! We therefore exclude `micro` and `Show-PSEditor`: both have custom handling below, and
# !! the latter - as a PowerShell cmdlet rather than an external binary - isn't subject to the problem to begin with.
$mustDelegateToViEditVisually = -not $isGuiEditor -and $editorBaseName -notin 'micro', 'Show-PSEditor'
if ($VerbosePreference -eq 'Continue') {
[pscustomobject] @{
Editor = $editor
Options = $options
EditorBaseName = $editorBaseName
IsGuiEditor = $isGuiEditor
MustDelegateToViEditVisually = $mustDelegateToViEditVisually
NeedTempFile = $needTempFile
} | Out-String | Write-Verbose -Verbose
}
if ($mustDelegateToViEditVisually) {
# !! If options were baked into the $env:VISUAL / $env:EDITOR value, invocation of `ViEditVisually` will fail
# !! up to at least v2.3.5 of PSReadLine
if ($options) { Write-Warning "Including pass-through options in `$env:VISUAL or `$env:EDITOR doesn't work up to at least PSReadLine 2.3.5" }
# !! Using `[Microsoft.PowerShell.PSConsoleReadLine]::ViEditVisually()` as of PSReadLine 2.3.5
# !! means that passing the command-line cursor position is NOT supported, and that the editor's cursor position
# !! will inevitably be at the *start* of the editor's buffer.
[Microsoft.PowerShell.PSConsoleReadLine]::ViEditVisually()
# !! When we delegate to the PSReadline function, we cannot know whether the buffer was modified,
# !! so we do NOT auto-submit it.
# [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()
return
}
if ($needTempFile) {
# Write the buffer to a temporary file.
$tempFilePath = Join-Path ([IO.Path]::GetTempPath()) "psVisualEditTmp_$PID.ps1"
Set-Content -Encoding utf8 -LiteralPath $tempFilePath -Value $bufferText
$lastWriteTimeUtc = [datetime]::UtcNow # Close enough.
}
# Edit the command using the chosen editor.
if ($editorBaseName -eq 'micro') {
# `micro` supports passing the cursor pos. as well as pipeline-based
# modal editing:
# If its stdout output is redirected, saving the editor's buffer is neither
# needed nor supported, and the potentially modified buffer content is output to stdout.
$prevEnc = [Console]::OutputEncoding; [Console]::OutputEncoding = [Text.Utf8Encoding]::new()
try {
$newBufferText = ($bufferText | & $editor -tabstospaces on "+${lineNo}:${colNo}") -join "`n"
}
finally {
[Console]::OutputEncoding = $prevEnc
}
}
elseif ($editorBaseName -eq 'code') {
# VSCode: make sure that the relevant options are used, and pass the cursor pos.
if (-not $options) {
$options = '--new-window', '--wait', '--goto'
}
$fileArg = if ($options[-1] -eq '--goto') { "${tempFilePath}:${lineNo}:${colNo}" } else { $tempFilePath }
& $editor @options $fileArg
}
# elseif ($editorBaseName -eq 'mcedit') {
# !! We cannot handle `mcedit` (the editor that comes with `mc`, GNU's Midnight Commander) here,
# !! due to the stdout redirection problem. In principle, as of v4.8.31, its CLI suppors positioning the cursor
# !! only by *line* number, with the cursor invariably placed in the *first column* of that line.
# !! e.g., `mcedit +5 foo.ps1`
# }
else {
# All other (GUI) editors: just pass the temp. file's path.
# Note: Due to lack of a unified mechanism, passing the cursor pos. is NOT supported,
# and the cursor will be at the *start* of the buffer.
& $editor @options $tempFilePath
}
if ($needTempFile) {
# Get the edited content from the temporary file and join its lines
# explicitly with "`n", because on Windows the CR chars. would cause problems.
$newBufferText = (Get-Content -LiteralPath $tempFilePath) -join "`n"
$lastWriteTimeUtcAfter = (Get-Item -LiteralPath $tempFilePath).LastWriteTimeUtc
Remove-Item $tempFilePath # Clean up.
}
$bufferModified = if ($needTempFile) { $lastWriteTimeUtcAfter -gt $lastWriteTimeUtc }
else { $newBufferText -cne $bufferText }
Write-Verbose "Buffer modified: $bufferModified"
# If the buffer text was modified in the editor, replace the command-line buffer with it and submit it.
# Otherwise, do nothing (except possibly reactive the terminal application below.)
if ($bufferModified) {
# Replace the current buffer text with the text from the file.
[Microsoft.PowerShell.PSConsoleReadLine]::Replace(0, $bufferText.Length, $newBufferText)
# As Bash does - but unlike the ViEditVisually function - automatically submit the command.
[Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()
}
# macOS only: If a GUI editor was used, we must explicitly reactivate the terminal.
if ($isGuiEditor -and $IsMacOS) {
Write-Verbose "macOS: Reactivating terminal app."
# !! On macOS, when VSCode is used (or another GUI editor, as opposed to a terminal-based editor),
# !! the terminal application typically does NOT reactivate after the editor window closes (if you either close the window only or if other windows are open),
# !! so even if the user doesn't manually reactivate another window, explicit reactivation of the terminal is necessary.
# !! Unfortunately, this is fairly slow.
$terminalAppName = $env:TERM_PROGRAM
# Terminal.app reports itself as 'Apple_Terminal', so we have to change it to 'Terminal.app'
# iTerm2 reports its actual app-bundle name, 'iTerm.app', in $env:TERM_PROGRAM, and we assume
# that any other terminal emulators do the same - if not, reactivation will fail quietly.
if ($terminalAppName -eq 'Apple_Terminal') { $terminalAppName = 'Terminal.app' }
$PSNativeCommandArgumentPassing = 'Legacy'
osascript -e "tell application \`"$terminalAppName\`" to activate" 2>$null
# Note:
# On Windows and Linux, reactivating the terminal is not strictly needed, because VSCode there doesn't have the wrong-window activation problem that it has on macOS.
# Conceivably, as a courtesy feature it could still be attempted, to allow users to switch to other windows while editing (e.g., for a web search),
# however:
# * On Windows, something like `(New-Object -ComObject WScript.Shell).AppActivate($PID)` would only work if AHK is running,
# or some other utility that allows non-foreground processes to steal the focus, and even then it would only work in `conhost.exe` windows, not also in
# in Windows Terminal.
}
}
but adding the handler seems to work without being in any specific mode.
I didn't know that this handler could work in Windows edit mode. Now the F4
key launches mcedit
to edit the command line. This has both convenience and inconvenience. You cannot copy a string from a console window, but this eliminates the risk of accidentally pressing Escape
and losing the entire script block.
UPD:
You cannot copy a string from a console window
You can copy strings from the console host with ^O
, it opens a bash console but keeps the content that was there before the editor was called.
This issue has been marked as answered and has not had any activity for 1 day. It has been closed for housekeeping purposes.
📣 Hey @jimbobmcgee, how did we do? We would love to hear your feedback with the link below! 🗣️
🔗 https://aka.ms/PSRepoFeedback
@kilasuit
a profile script that you can access on any/all servers you work on
Implies that such a script is allowed to exist; that all servers I work on are happily interconnected and can access the same resources.
If only...
But I understand the notion that this could be easily included in a profile script.
Summary of the new feature / enhancement
As a user, I frequently find myself starting what I think will be a simple command in Powershell, only to find it evolve into a multi-pipeline, multi-bracket hellbeast which spans multiple lines and is impossible to navigate through, and I have to throw my hands up in the air, bang the whole thing into the clipboard (using the mouse of all things) and paste it into an editor.
Of course, then I have to fight any punctuation/newlines introduced by console window's copy/paste (I'm looking at you
>>
line-leader), hope that I caught them all (and escaped the ones I meant to leave in), and paste the completed command back to the console window. Nine times out of ten, I'll end up highlighting a single blank space in the console window, overwriting the clipboard, and have to go back to the editor to copy again.All of this is fraught, and it would be nice if a massive chunk of that busywork could be taken away.
Many interactive/terminal apps have the concept of opening a preconfigured visual editor, pre-populated with the command currently in the "buffer" as a temp file, and letting you edit that temp file in the visual editor. When the editor is exited, if the temp file was saved, the content is fed back into the buffer, ready to be executed.
Examples are the prominent database-interacting terminal apps -- consider
:ED
in SQL Server's sqlcmd;ed
in Oracle's sqlplus; or\e
in PostgreSQL's psql.I would like this same facility for PowerShell, and I would like to bind it to a keypress or combo. So, if I press (for example) F8 notepad.exe automatically opens with the current command automagically within and lets me edit. I save and exit notepad.exe, the command buffer is automagically updated and I'm positioned at the end, ideally just before the point where I would normally press Enter and have it execute. If I didn't save the buffer file, the command wouldn't be updated, and would remain unaffected.
This would differ slightly from the above examples: notably those mentioned terminal apps all operate on a buffer of data that is submitted line-by-line, but not executed until a sentinel command is entered; whereas I would prefer that pressing the invocation key opens the current, unsubmitted command. Unfortunately, I am not certain how you would go about obtaining (and subsequently rewriting) the unsubmitted command buffer (but perhaps this is trivial and I am worrying unnecessarily?).
For style points, allow the editor to be in someway configurable -- maybe follow EDITOR or VISUAL environment variables as per non-Windows (or introduce your own
$PSEditor
); perhaps defaulting to notepad.exe for Windows; and vi for POSIX, if these are not set. I assume that , if following the EDITOR/VISUAL approach, it would allow for any command which takes the path to the buffered temp file as its last argument.Obviously not all editor apps would be feasible for this (one might struggle with tracking the closure of the temp file if the editor were an MDI/IDE-type app), but that can be documented if it seems non-obvious.
For more style points, I would allow the invocation to operate on any historic command: i.e. allow for navigating to a previous command with ↑/↓, then hitting the invocation key puts that command in the buffer temp file and opens the editor; only, this time, saving the file would result in a new command, not overwriting the historic one.
I'm sure there is some nuance I haven't considered, but am happy to flesh it out over time.
Proposed technical implementation details (optional)
No response