How to validate powershell conditional syntax? - powershell

Is there a syntax checker for conditionals in PowerShell?
This code will skip the conditional
if ($templList -ne $null -and $templList.items.Length > 0) {
$templID=$templList.items[0].id
write-host $templList.items
}
because the '-gt' is substituted with '>'.

Strictly speaking, you're looking for a semantics checker, given that your code is syntactically correct.
The problem here is that while the code was formally correct, it doesn't do what you intended it to do.
PSScriptAnalyzer (PSSA) is a linter for PowerShell, and it is integrated into the PowerShell extension for Visual Studio Code.
It would catch your problem, emitting the following message:
Did you mean to use the redirection operator '>'?
The comparison operators in PowerShell are '-gt' (greater than) or '-ge' (greater or equal).
Use in Visual Studio Code:
Install the PowerShell extension.
Once installed, with your script opened for editing, you'll see the > 0 part underlined, and hovering over it shows the message as a tooltip.
You can see all messages for the current file in the Problems view (Ctrl-Shift-M or, via the menus, View > Problems), which incidentally, will show you that PSSA found additional potential problems with your code snippet.
To configure what rules to apply (which potential problems to warn about), use >PowerShell: Select PSScriptAnalyzer Rules from the command palette.
The rule names, such as PSAvoidGlobalVars, are mostly self-explanatory; the rules documentation describes them (without the PS prefix); and there's also a best practices topic.
Important:
After checking / unchecking (or pressing Enter on) the rules of interest, be sure to press Enter on the Confirm menu item at the top, otherwise your changes won't take effect.
As of v2019.11.0, these selections aren't persisted (are remembered in the current session only) - see this GitHub issue
Note that PSSA also offers automatic (re)formatting of PowerShell code (Alt-Shift-F or, via the command palette, >Format Document).
Stand-alone use:
Install the PSScriptAnalyzer module from the PowerShell Gallery; e.g.:
Install-Module -Scope CurrentUser PSScriptAnalyzer
The module comes with the following cmdlets:
Invoke-ScriptAnalyzer ... lints script files.
Invoke-Formatter ... reformats code passed as a string.
Get-ScriptAnalyzerRule ... lists standard and optionally custom analyzer rules.
Pass your script's path to the Invoke-ScriptAnalyzer cmdlet to perform linting.
With your code, you'd see the following output (having been given a script named pg.ps1):
RuleName Severity ScriptName Line Message
-------- -------- ---------- ---- -------
PSPossibleIncorrectUsageOfRedirecti Warning pg.ps1 1 Did you mean to use the redirection operator '>'? The
onOperator comparison operators in PowerShell are '-gt' (greater than)
or '-ge' (greater or equal).
PSPossibleIncorrectComparisonWithNu Warning pg.ps1 1 $null should be on the left side of equality comparisons.
ll
PSUseDeclaredVarsMoreThanAssignment Warning pg.ps1 2 The variable 'templID' is assigned but never used.
s
PSAvoidUsingWriteHost Warning pg.ps1 3 File 'pg.ps1' uses Write-Host. Avoid using Write-Host
because it might not work in all hosts, does not work when
there is no host, and (prior to PS 5.0) cannot be
suppressed, captured, or redirected. Instead, use
Write-Output, Write-Verbose, or Write-Information.

Related

PowerShell 7.3.0 breaking command invocation

I use WinSCP within a Powershell script. It suddenly stopped working. After a while I could figure out that the problem appeared from a more recent version of PowerShell:
Reduced code:
& winscp `
/log `
/command `
'echo Connecting...' `
"open sftp://kjhgk:jkgh#lkjhlk.com/ -hostkey=`"`"ssh-ed25519 includes spaces`"`""
Error message using v7.2.7
Host "lkjhlk.com" does not exist.
Errror message using v7.3.0
Too many parameters for command 'open'.
As you can see with v7.3.0 WinSCP receives different input depending on the version of PS. I found out that the difference has something to do with the spaces in the hostkey. If they are omitted v7.3.0 outputs the same error.
What change to PowerShell caused this, and how can I fix it?
(How can I debug such issues? I played a bit around with escaping, but the strings look the same no matter the version, no obvious breaking change that could be responsible)
Version 7.3.0 of PowerShell (Core) introduced a breaking change with respect to how arguments with embedded " characters are passed to external programs, such as winscp:
While this change is mostly beneficial, because it fixes behavior that was fundamentally broken since v1 (this answer discusses the old, broken behavior), it also invariably breaks existing workarounds that build on the broken behavior, except those for calls to batch files and the WSH CLIs (wscript.exe and cscript.exe) and their associated script files (with file-name extensions such as .vbs and .js).
CAVEAT:
GitHub issue #18694 implies that some version after 7.3.1 and all later versions will make this breaking change opt-in, but on Windows only, for the sake of backward compatibility; that is:
On Windows:
Old workarounds will continue to work by default.
Getting the new, correct behavior requires (temporarily) setting
$PSNativeCommandArgumentPassing = 'Standard'
On Unix-like platforms:
The new, correct behavior ($PSNativeCommandArgumentPassing = 'Standard') will remain the default
Old workarounds will require (temporarily) setting $PSNativeCommandArgumentPassing = 'Legacy' in order to continue to work, as is already the case in 7.3.0
To make existing workarounds continue to work, set the $PSNativeCommandArgumentPassing preference variable (temporarily) to 'Legacy':
# Note: Enclosing the call in & { ... } makes it execute in a *child scope*
# limiting the change to $PSNativeCommandArgumentPassing to that scope.
& {
$PSNativeCommandArgumentPassing = 'Legacy'
& winscp `
/log `
/command `
'echo Connecting...' `
"open sftp://kjhgk:jkgh#lkjhlk.com/ -hostkey=`"`"ssh-ed25519 includes spaces`"`""
}
Unfortunately, because winscp.exe only accepts
"open sftp://kjhgk:jkgh#lkjhlk.com/ -hostkey=""ssh-ed25519 includes spaces""" on its process command line (i.e., embedded " escaped as ""), and not also the most widely used form
"open sftp://kjhgk:jkgh#lkjhlk.com/ -hostkey=\"ssh-ed25519 includes spaces\"" (embedded " escaped as \"), which the fixed behavior now employs, for winscp.exe, specifically, a workaround will continue to be required.
If you don't want to rely on having to modify $PSNativeCommandArgumentPassing for the workaround, here are workarounds that function in both v7.2- and v7.3+ :
Use --%, the stop-parsing token, which, however, comes with pitfalls and severe limitations, notably the inability to (directly) use PowerShell variables or subexpressions in the arguments that follow it - see this answer for details; however, you can bypass these limitations if you use --% as part of an array that you construct and assign to a variable first and then pass via splatting:
# Note: Must be single-line; note the --% and the
# unescaped use of "" in the argument that follows it.
# Only "..." quoting must be used after --%
# and the only variables that can be used are cmd-style
# *environment variables* such as %OS%.
winscp /log /command 'echo Connecting...' --% "open sftp://kjhgk:jkgh#lkjhlk.com/ -hostkey=""ssh-ed25519 includes spaces"""
# Superior alternative, using splatting:
$argList = '/log', '/command', 'echo Connecting...',
'--%', "open sftp://kjhgk:jkgh#lkjhlk.com/ -hostkey=""ssh-ed25519 includes spaces"""
winscp #argList
Alternatively, call via cmd /c:
# Note: Pass-through command must be single-line,
# Only "..." quoting supported,
# and the embedded command must obey cmd.exe's syntax rules.
cmd /c #"
winscp /log /command "echo Connecting..." "open sftp://kjhgk:jkgh#lkjhlk.com/ -hostkey=""ssh-ed25519 includes spaces"""
"#
Note: You don't strictly need to use a here-string (#"<newline>...<newline>"# or #'<newline>...<newline>'#), but it helps readability and simplifies using embedded quoting.
Both workarounds allow you to pass arguments directly as quoted, but unfortunately also require formulating the entire (pass-through) command on a single line - except if --% is combined with splatting.
Background information:
The v7.3 default $PSNativeCommandArgumentPassing value on Windows, 'Windows':
regrettably retains the old, broken behavior for calls to batch files and the WSH CLIs (wscript.exe and cscript.exe) and their associated script files (with file-name extensions such as .vbs and .js).
While, for these programs only, this allows existing workarounds to continue to function, future code that only needs to run in v7.3+ will continue to be burdened by the need for these obscure workarounds, which build on broken behavior.
The alternative, which was not implemented, would have been to build accommodations for these programs as well as some program-agnostic accommodations into PowerShell, so that in the vast majority of case there won't even be a need for workarounds in the future: see GitHub issue #15143.
There are also troublesome signs that this list of exceptions will be appended to, piecemeal, which all but guarantees confusion for a given PowerShell version as to which programs require workarounds and which don't.
commendably, for all other programs, makes PowerShell encode the arguments when it - of necessity - rebuilds the command line behind the scenes as follows with respect to ":
It encodes the arguments for programs that follow the C++ command-line parsing rules (as used by C / C++ / .NET applications) / the parsing rules of the CommandLineToArgv WinAPI function, which are the most widely observed convention for parsing a process' command line.
In a nutshell, this means that embedded " characters embedded in an argument, to be seen as a verbatim part of it by the target program, are escaped as \", with \ itself requiring escaping only (as \\) if it precedes a " but is meant to be interpreted verbatim.
Note that if you set $PSNativeCommandArgumentPassing value to 'Standard' (which is the default on Unix-like platforms, where this mode fixes all problems and makes v7.3+ code never require workarounds), this behavior applies to all external programs, i.e. the above exceptions no longer apply).
For a summary of the impact of the breaking v7.3 change, see this comment on GitHub.
If you have / need to write cross-edition, cross-version PowerShell code: The Native module (Install-Module Native; authored by me), has an ie function (short for: Invoke Executable), which is a polyfill that provides workaround-free cross-edition (v3+), cross-platform, and cross-version behavior in the vast majority of cases - simply prepend ie to your external-program calls.
Caveat: In the specific case at hand it will not work, because it isn't aware that winscp.exe requires ""-escaping.

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.

PowerShell fails to interpret commands such as "which"

My windows Powershell gives the expected results when interpreting commands such as "date" and "echo", while problems occur when it interprets such commands as "which" and "tail".
I thought it might be that I haven't add the address of these commands to the target directories, but where can I probably find these commands.
What happens when I apply "which", with the same thing happening when applying "tail"
which and tail are the names of external programs, which are unrelated to PowerShell. These programs can only be expected to be present on Unix-like platforms, not (natively) on Windows. (However, they would be present in Unix-like subsystems on Windows, such as WSL.)
By contrast, date and echo are (potentially) commands that are built into PowerShell, irrespective of what platforms it runs on:
echo is an alias of PowerShell's Write-Output cmdlet on all platforms.
date - unless an external program by that name is present in a directory listed in $env:PATH on a given platform - refers to the built in Get-Date cmdlet.
Note: This relies on PowerShell's ill-conceived default verb feature, which falls back on prefixing a command name with Get- if no command form is found by the given name. This shouldn't be relied upon, both in the interest of conceptual clarity and to avoid unnecessary overhead. Also, this fallback is (unexpectedly) not reported by the Get-Command discussed below, and also not by Get-Help - see GitHub issue #3987.
Use Get-Command to determine what command form, if any, a given name refers to (add -All to see if potentially multiple forms exist, with the effective one being listed first).

Same script, same console, why do colors work in Powershell but not cmd.exe? [duplicate]

I wrote a program which prints a string, which contains ANSI escape sequences to make the text colored. But it doesn't work as expected in the default Windows 10 console, as you can see in the screenshot.
The program output appears with the escape sequences as printed characters.
If I feed that string to PowerShell via a variable or piping, the output appears as intended (red text).
How can I achieve that the program prints colored text without any workarounds?
This is my program source (Haskell) - but the language is not relevant, just so you can see how the escape sequences are written.
main = do
let red = "\ESC[31m"
let reset = "\ESC[39m"
putStrLn $ red ++ "RED" ++ reset
Note:
The following applies to regular (legacy) console windows on Windows (provided by conhost.exe), which are used by default, including when a console application is launched from a GUI application.
By contrast, the console windows (terminals) provided by Windows Terminal as well as Visual Studio Code's integrated terminal provide support for VT / ANSI escape sequences by default, for all console applications.
While console windows in Windows 10 do support VT (Virtual Terminal) / ANSI escape sequences in principle, support is turned OFF by default.
You have three options:
(a) Activate support globally by default, persistently, via the registry, as detailed in this SU answer.
In short: In registry key [HKEY_CURRENT_USER\Console], create or set the VirtualTerminalLevel DWORD value to 1
From PowerShell, you can do this programmatically as follows:
Set-ItemProperty HKCU:\Console VirtualTerminalLevel -Type DWORD 1
From cmd.exe (also works from PowerShell):
reg add HKCU\Console /v VirtualTerminalLevel /t REG_DWORD /d 1
Open a new console window for changes to take effect.
See caveats below.
(b) Activate support from inside your program, for that program (process) only, with a call to the SetConsoleMode() Windows API function.
See details below.
(c) Ad-hoc workaround, from PowerShell:
PowerShell (Core) 7+: Enclose external-program calls in (...) (invariably collects all output first before printing):
(.\test.exe)
Streaming Windows PowerShell-only alternative: Pipe output from external programs to Write-Host
.\test.exe | Out-Host
See details below.
Re (a):
The registry-based approach invariably activates VT support globally, i.e., for all console windows, irrespective of what shell / program runs in them:
Individual executables / shells can still deactivate support for themselves, if desired, using method (b).
Conversely, however, this means that the output of any program that doesn't explicitly control VT support will be subject to interpretation of VT sequences; while this is generally desirable, hypothetically this could lead to misinterpretation of output from programs that accidentally produce output with VT-like sequences.
Note:
While there is a mechanism that allows console-window settings to be scoped by startup executable / window title, via subkeys of [HKEY_CURRENT_USR\Console], the VirtualTerminalLevel value seems not to be supported there.
Even if it were, however, it wouldn't be a robust solution, because opening a console window via a shortcut file (*.lnk) (e.g. from the Start Menu or Task Bar) wouldn't respect these settings, because *.lnk files have settings built into them; while you can modify these built-in settings via the Properties GUI dialog, as of this writing the VirtualTerminalLevel setting is not surfaced in that GUI.
Re (b):
Calling the SetConsoleMode() Windows API function from inside the program (process), as shown here, is cumbersome even in C# (due to requiring P/Invoke declarations), and may not be an option:
for programs written in languages from which calling the Windows API is not supported.
if you have a preexisting executable that you cannot modify.
In that event, option (c) (from PowerShell), discussed next, may work for you.
Re (c):
PowerShell automatically activates VT (virtual terminal) support for itself when it starts (in recent releases of Windows 10 this applies to both Windows PowerShell and PowerShell (Core) 7+) - but that does not extend to external programs called from PowerShell, in either edition, as of v7.3.2.
Separately, in v7.2+ there is the $PSStyle.OutputRendering preference variable, which controls whether PowerShell commands produce colored output via the formatting system, such as the colored headers of Get-ChildItem output. However, this setting has no effect on (direct) output from external programs. $PSStyle.OutputRendering defaults to Host, meaning that only formatted output that prints to the terminal (console) is colored. $PSStyle.OutputRendering = 'PlainText' disables coloring, and $PSStyle.OutputRendering = 'Ansi' makes it unconditional; see this answer for more information.
However, as a workaround you can relay an external program's (stdout) output via PowerShell's (success) output stream, in which case VT sequences are recognized:
As of PowerShell (Core) 7.3.2, this only works either by enclosing the call in (...) or by using Out-String, but note that all output is invariably collected first before it is printed.[1]
(.\test.exe)
In Windows PowerShell, in addition to the above, streaming the relayed output is possible too, by piping to Write-Host (Out-Host, Write-Output or Out-String -Stream would work too)
.\test.exe | Write-Host
Note: You need these techniques only if you want to print to the console. If, by contrast, you want to capture the external program's output (including the escape sequences), use $capturedOutput = .\test.exe
Character-encoding caveat: Windows PowerShell by default expects output from external programs to use the OEM code page, as defined by the legacy system locale (e.g., 437 on US-English systems) and as reflected in [console]::OutputEncoding.
.NET console programs respect that setting automatically, but for non-.NET programs (e.g., Python scripts) that use a different encoding (and produce not just pure ASCII output (in the 7-bit range)), you must (at least temporarily) specify that encoding by assigning to [console]::OutputEncoding; e.g., for UTF-8:
[console]::OutputEncoding = [Text.Encoding]::Utf8.
Note that this is not only necessary for the VT-sequences workaround, but generally necessary for PowerShell to interpret non-ASCII characters correctly.
PowerShell Core (v6+), unfortunately, as of v7.3.2, still defaults to the OEM code page too, but that should be considered a bug (see GitHub issue #7233), given that it otherwise defaults to UTF-8 without BOM.
[1] Using Out-String -Stream or its built-in wrapper function, oss, is tempting in order to achieve streaming output, but this no longer works as of PowerShell 7.3.2, possibly due to the optimization implemented in GitHub PR #16612.
Thanks to user mklement0 for making me aware that VT support is not enabled automatically. This made me look in the right direction and I found this helpful post.
So to answer my question: Add or set the registry key
HKCU:\Console - [DWORD] VirtualTerminalLevel = 1
Restart the console and it works.
For Haskell specifically, you need to call the hSupportsANSIWithoutEmulation function to enable ANSI escape sequences in your program on Windows 10.

How to get full command from doskey alias in Windows PowerShell

I'm using Windows PowerShell and it is configured using doskey macros.
I have a file named Macros.doskey.
105=code E:\static5\105
135=code E:\static5\135
static5=code E:\static5
How can I get the command corresponding to alias name ?
For example, when I type 105 in PowerShell it will execute the command code E:\static5\105
Now I want to know how to get the command from the alias name.
doskey is a utility primarily designed to work with cmd.exe, not PowerShell.
PowerShell has better functionality built in, in the form of aliases and functions, and in Windows 10 you must even deactivate PowerShell's own command-line editing to even get doskey to work (see below).
If you still want to use doskey in PowerShell, there are two prerequisites:
The PSReadLine module - which handles command-line editing since Windows 10 by default - must not be loaded, as it takes precedence over doskey definitions[1]; that is, you may need to unload it explicitly with Remove-Module PSReadLine, but that means that you'll lose all of its benefits.
You must invoke any doskey.exe macro definitions with /exename=powershell.exe (Windows PowerShell) or /exename=pwsh.exe (PowerShell Core) for them to be usable from PowerShell.
Note that it is then doskey that expands a macro name typed by the user, which means that PowerShell only sees the expanded command and therefore has no knowledge of macro names. Therefore, trying to inspect doskey macros with Get-Command won't work; inspect the output from doskey /macros instead, as in Lee Dailey's answer.
Additionally, doskey also resolves macros when soliciting arbitrary user input via Read-Host[1], which is undesired.
To summarize the reasons for not using doskey in PowerShell:
It cannot be used with the PSReadLine module, which by default handles command-line editing since Windows 10 and provides invaluable functionality.
doskey macro expansion invariably also occurs when scripts solicit arbitrary user input via Read-Host, which is undesired.
Therefore, I suggest you abandon doskey in favor of PowerShell functions, and add them to your $PROFILE file so that they're available in every session:
While you can define functions named for numbers such as 105in PowerShell, you'll have to invoke them with & so as to disambiguate from actual numbers, e.g., & 105.
Therefore, I suggest refactoring your approach to define a single function named, say, c, that takes an optional argument to identify which file(s) to open:
function c { pushd E:/static5; code $(if ($Args) { $Args } else { '.' }); popd }
Your original doskey macros then map onto this function as follows:
105 -> c 105
135 -> c 135
static5 -> c
Note that this not only allows you to pass an arbitrary file name (of a file located in E:/static5/) to function c, but even multiple ones; e.g., c 105 135 would open both files for editing.
To inspect the definition of function c later, you can simply call $function:c or, more verbosely, (Get-Command c).Definition.
[1] As PetSerAl notes: "doskey performs translations on the console input buffer. [...]. It does not work if the console is not in line input mode, thus it is not compatible with PSReadline, although Read-Host will be affected.
https://i.stack.imgur.com/HpYzq.png"
I've never used doskey with PowerShell, so I'm not sure how they appear. My best guess is that PowerShell sees them as commands of type "Application" which usually means external executables.
To start, check what PowerShell sees:
Get-Command 105 | Format-List *
# or
gcm 105 | fl *
From there, you might see just some exe like doskey.exe or maybe you'll see additional info too.
If it's just listed as an executable then I'm not sure you'll be able to get the info out of doskey.
PowerShell has its own versions of aliases which would have let you do full discovery, but PowerShell aliases can't take parameters, they just alias one command to another.
You could simulate doskey functionality in PowerShell by writing functions instead:
function static5 {
code.exe 'E:\static5'
}
this will do the job fairly well. [grin] note that it just gives the text matching the macro. i did not test with multiline macros, nor do i know if that is even possible.
if you want the result to be just the macro instead of the full line, you can add a | ForEach-Object {$_.Split('=')[1]} to get the part after the =.
#"
404=echo '404'
666=echo '666'
dwd=echo 'Doo Wa Diddy'
"# | Set-Content "$env:TEMP\Doskey-Macros.txt" -Force
doskey /macrofile=c:\temp\doskey-macros.txt
function DKA ($Macro)
{
doskey /macros |
Where-Object {
$_ -match $Macro
}
}
testing & output ...
dka -Macro dwd
dwd=echo 'Doo Wa Diddy'
dka -Macro adg
# nothing at all since there was no match
dka 404
404=echo '404'