How do you edit the command line in an external editor? - powershell

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

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

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.

Related

PowerShell command to set Visual Studio to Run as Administrator

I have been assigned the job of automating a long list of tasks for developers to load into Intune.
One of the tasks is to set the Run As Administrator feature to Visual Studio with a script.
I know you can right-click on the exe, go to properties > Advanced and set that setting, but I'm struggling to find a scriptable way to do that. Any suggestions would be appreciated.
Edit: This to update the shortcut to send the command to run as administrator.
The shell-friendly way to create shortcut files (*.lnk) unfortunately does not provide a way to mark a shortcut file as needing to run with elevation (as administrator).
To do so, you have two options:
A quick-and-dirty solution based on raw byte manipulation of a .lnk file.
The caveat is that such an approach may break in a future Windows version, should the binary file format of shortcut file changes. See below.
A proper solution via the appropriate COM interfaces, which are not script-friendly, however.
See this blog post for C++ sample code.
Quick-and-dirty solution, effective as of Windows 10, gratefully adapted from this answer:
# Get the *full path* of the target shortcut file (*.lnk),
# using sample path ~/Desktop/test.lnk here.
$shortcutFile = Convert-Path -LiteralPath (Get-Item ~/Desktop/test.lnk)
# Read the shortcut file's raw byte contents.
$bytes = [System.IO.File]::ReadAllBytes($shortcutFile)
# Set the relevant byte to mark the shortcut as needing to
# run with elevation.
$bytes[21] = 34
# Save the modified byte array back to the original file.
[System.IO.File]::WriteAllBytes($shortCutLocation, $bytes)

How to make parameter name suggestions work?

I'm trying to create a powershell module to store some reusable utility functions. I created script module PsUtils.psm1 and script module manifest PsUtils.psd1 (used these docs). My problem is that when I import this module in another script Visual Code does not suggest parameters names. Here's a screenshot:
When I hover cursor over the function I only get this:
PsUtils.psm1
function Get-Filelist {
Param(
[Parameter(Mandatory=$true)]
[string[]]
$DirectoryPath
)
Write-Host "DIR PATH: $DirectoryPath"
}
PsUtils.psd1 (excerpt)
...
FunctionsToExport = '*'
I have Powershell extension installed. Do I need to install anything else to make the suggestions work? What am I missing?
Generally speaking, only auto-loading modules - i.e., those in one of the directories listed in environment variable $env:PSModulePath - are automatically discovered.
As of version v2022.7.2 of the PowerShell extension, the underlying PowerShell editor services make no attempt to infer from the current source-code file what modules in nonstandard directories are being imported via source code in that file, whether via Import-Module or using module
Doing so would be the prerequisite for discovering the commands exported by the modules being imported.
Doing so robustly sounds virtually impossible to do with the static analysis that the editor services are limited to performing, although it could work in simple cases; if I were to guess, such a feature request wouldn't be entertained, but you can always ask.
Workarounds:
Once you have imported a given module from a nonstandard location into the current session - either manually via the PIC (PowerShell Integrated Console) or by running your script (assuming the Import-Module call succeeds), the editor will provide IntelliSense for its exported commands from that point on, so your options are (use one of them):
Run your script in the debugger at least once before you start editing. You can place a breakpoint right after the Import-Module call and abort the run afterwards - the only prerequisite is that the file must be syntactically valid.
Run your Import-Module command manually in the PIC, replacing $PSScriptRoot with your script file's directory path.
Note: It is tempting to place the cursor on the Import-Module line in the script in order to use F8 to run just this statement, but, as of v2022.7.2, this won't work in your case, because $PSScriptRoot is only valid in the context of running an entire script.
GitHub issue #633 suggests adding special support for $PSScriptRoot; while the proposal has been green-lighted, no one has stepped up to implement it since.
(Temporarily) modify the $env:PSModulePath variable to include the path of your script file's directory.
The most convenient way to do that is via the $PROFILE file that is specific to the PowerShell extension, which you can open for editing with psedit $PROFILE from the PIC.
Note: Make sure that profile loading is enabled in the PowerShell extension's settings.
E.g., if your directory path is /path/to/my/module, add the following:
$env:PSModulePath+="$([IO.Path]::PathSeparator)/path/to/my/module"
The caveat is that all scripts / code that is run in the PIC will see this updated $env:PSModulePath value, so at least hypothetically other code could end up mistakenly importing your module instead of one expected to be in the standard locations.
Note that GitHub issue #880 is an (old) proposal to allow specifying $env:PSModulePath entries as part of the PowerShell extension settings instead.
On a somewhat related note:
Even when a module is auto-discovered / has been imported, IntelliSense only covers its exported commands, whereas while you're developing that module you'd also like to see its private commands. Overcoming this limitation is the subject of GitHub issue #104.

How to open a file from the integrated terminal in VSCode to a new tab

If my script is run within vscode, it want it to open a .txt file in a new tab in vscode. Else, open the folder containing the file. However, the current "code" command opens it in the terminal window instead of a new edit tab.
if ($env:TERM_PROGRAM -eq 'vscode') {
code 'C:\temp\_Release_Evidence\test.txt'
}
else {
explorer 'C:\temp\_Release_Evidence'
}
Normally, code refers Visual Studio Code's CLI, which is assumed to be in one of the directories listed in $env:PATH:
On Windows, it refers to the code.cmd batch file that serves as the CLI entry point.
On Unix-like platforms it refers to a code Bash script.
Its default behavior is to open a given file as a new tab in the most recently activated Visual Studio Code window (which, when run from inside Visual Studio Code, is by definition the current window).
If that doesn't happen for you, perhaps code refers to a different executable on your machine:
To avoid ambiguity, use the full CLI path instead, which, however, requires you to know Visual Studio Code's install location; typical CLI locations are:
Windows: $env:LOCALAPPDATA\Programs\Microsoft VS Code\bin\code.cmd
macOS: /usr/local/bin/code
Linux: /usr/bin/code
On Windows, something as simple as including the filename extension in the invocation - i.e., code.cmd - may help.
However, assuming you're using the PIC (PowerShell Integrated Console), a specialized PowerShell shell that comes with the PowerShell extension for Visual Studio Code, a workaround that also performs better, because it doesn't require launching a child process:
The PIC comes with the psedit command (an alias for the Open-EditorFile function), which quickly opens one or more files in a tab in the current Visual Studio Code window.
Caveat: As of version v2022.5.1 of the PIC, specifying multiple files only works meaningfully if they are individually enumerated, as literal paths. If you try to use a wildcard pattern or a directory path, all matching files / files in the directory are uselessly opened in sequence in a single tab.
Thus, you could use the following:
if ($env:TERM_PROGRAM -eq 'vscode') {
# Use `psedit`, if available; `code` otherwise.
$exe = if ((Get-Command -ErrorAction Ignore psedit)) { 'psedit' } else { 'code' }
& $exe 'C:\temp\_Release_Evidence\test.txt'
}
else {
explorer 'C:\temp\_Release_Evidence'
}
I can't reproduce this or explain why this might occur on your system. Running the following whether in the PowerShell Integrated Terminal (which #mklement0 explained succinctly) or a standard PowerShell terminal in VS Code's Terminal pane should open the file in a new tab where file contents are normally displayed:
code /path/to/file.txt
A suitable workaround may be to get the contents of a text file and pipe them in via STDIN. We can do this by adding a hyphen - as an empty parameter to code when piping data to it:
# Tip: Use the gc alias for Get-Content
Get-Content /path/to/file.txt | code -
You can then use Save As... to save the file to its intended target once you make your changes. You will need to use Ctrl+C in the terminal to close the input stream if you need to run additional commands before closing the file or saving to a one.
Even if this isn't a suitable workaround for you, it's a handy tip in other scenarios. For example, the following command will open documentation for Get-Process inside VSCode:
Reminder: Don't forget to hit Ctrl+C in the terminal once the content finishes populating to be able to run additional commands, or close the temporary file buffer.
Get-Help Get-Process -Detailed | code -

Is it possible to add a keyboard shortcut to Powershell?

I've switched to Powershell after a couple of decades of bash, and after some configuring of my profile (and adding PSCX, openssl and a few other tools) I'm generally happy.
One thing I miss from bash:
mkdir some-very-long-dir-name
cd (hit ESC then hit _ on the keyboard)
Escape underscore is bash for 'last item on the previous command'. It's super useful - in this chase I didn't have to type out the very long directory name.
Is it possible to add keyboard shortcuts to powershell? How?
I'm using ConEmu as my terminal if that matters.
Short answer:
Set-PSReadlineKeyHandler -Key 'Escape,_' -Function YankLastArg
Longer explanation:
Thanks to #davidbrabant and #TheIncorrigible1 for pointing to PSReadLine: it's not the answer itself, but understanding how PSReadLine works is the key to solving this.
Although vi is the default editor on every Linux distribution, bash's default is emacs editing mode. From the bash docs:
In order to switch interactively between emacs and vi editing modes, use the ‘set -o emacs’ and ‘set -o vi’ commands (see The Set Builtin). The Readline default is emacs mode.
Which means 'escape underscore' comes from emacs.
Oddly, PSReadLine, unlike bash, does't use emacs mode by default. From the PSREADLine docs:
To use Emacs key bindings, you can use:
Set-PSReadlineOption -EditMode Emacs
It's not very explicit, but that means another mode is default. Confirming that, running:
get-PSReadlineOption
Returns:
EditMode : Vi
So there are two solutions:
Solution 1: change mode
Set-PSReadlineOption -EditMode Emacs
You can see the effect with Get-PSReadlineKeyHandler includes the standard escape underscore shortcut:
Escape,_ YankLastArg Copy the text of the last argument to the input
Escape underscore now works.
Solution 2: add the shortcut to your existing mode
Instead of changing mode (it turns out I like vi keybindings!), you can also run:
Set-PSReadlineKeyHandler -Key 'Escape,_' -Function YankLastArg
To add it to your existing mode.
An alternative to your ESC+_ solution, the PowerShell automatic variable $$ contains the same information without the need for PSReadLine (pre-v5.0 or without the module installed).
PS C:\> Get-ChildItem -Path 'C:\'
...
PS C:\> $$
C:\
You can also capture the command used with the $^ variable:
PS C:\> $^
get-childitem

Powershell ISE + vim

Is it possible to make Powershell ISE behave like vim with some vim-like editing mode or plugin?
In netbeans I'm using jVi, and in Visual Studio I'm using VsVim, is there something similar for PowerShell ISE?
Or should I drop Powershell ISE altogether and just use vim + powershell command line?
I may be misunderstanding the question, but the closest I've come to getting vim-like ability while using PS ISE is to install gvim74 for Windows, and create this function:
function vim
{
if ($args)
{
start-process 'C:\Program Files (x86)\Vim\vim74\vim.exe' $args
}
else
{
start-process 'C:\Program Files (x86)\Vim\vim74\vim.exe'
}
}
Unfortunately, ISE does not play nicely with console input, so the best I can get is starting vi in a new PS window. Not perfect, but I get my ability to "vim fileToEdit", which I do A LOT in non-Windows world.
The last weaks i searched for a way to use psISE in combination with vim (i found out its possible but hard for me to code without the ISE-comfort)
In the end i created a function/submenu in the ISE that:
With a keyboard-shortcut ([AltGr]+[v]) starts vim with the current file and waits for the process to end
When I'm finished editing in vim and saved the file its removed from the ISE
And loaded again (bacause in v3 there is no file "refreshing")
# one-line
$psISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Add("edit with Vim",{$cur=$psISE.CurrentFile; saps "C:\Program Files (x86)\vim\vim74\gvim.exe" $cur.FullPath -wait; $psise.currentpowershelltab.files.remove($cur); $psISE.currentpowershelltab.files.add($cur.fullpath) },'Ctrl+Alt+v')
You have to save it first or it will not be removed
(maybe you add the $psise.CurrentFile.Save()-function)
(for permanent ISE-changes you have to put it into the $profile...)
*saps --> start-process alias
The best thing I've found is VSCode with the PowerShell and VIM plugin installed. The Powershell plugin used to be a lot worse than ISE, but recently the Intellisense has gotten much cleaner.