2

I have written a powershell cmdlet that takes a path as parameter. The path may contain special characters like [ and spaces.

A user complained that the script wouldn't handle escaped paths when using auto completion. This is what powershell produces when auto-complete such a path:

.\myscript.ps1 '.\file`[bla`].dot'

My questions:

  1. Why does powershell auto completion use single-quotes and escapes special characters? Isn't that wrong? It should do either one or the other, but not both.
  2. What is the correct way to deal with such parameters?
Richard W
  • 631
  • 4
  • 15
  • 1
    Will do, but first I need to understand it. The thing is that I come from the UNIX world where these things used to be a no-brainer. – Richard W Nov 18 '20 at 14:17
  • Got it. FWIW, PowerShell's behavior is questionable here, and I will post an issue on GitHub to follow up. – mklement0 Nov 18 '20 at 14:20
  • Re Unix: The main difference is that in PowerShell you cannot use quoted vs. unquoted to distinguish between wildcard (globbing) patterns and literal names; e.g., `'*.txt'` and `*.txt` as command arguments behave the same, and it is up to the command being invoked to either treat it as a pattern or as a literal - hence the distinct `-Path` (pattern) and `-LiteralPath` (literal) parameters on file-processing cmdlets. – mklement0 Nov 18 '20 at 14:23

2 Answers2

2

Why does powershell auto completion use single-quotes and escapes special characters?

Because the `-escaping isn't string-interpolation escaping (which only applies to "..." strings), it is escaping of wildcard-pattern metacharacters, which happen to use the same escape character as string-interpolation escaping, `.
In other words: by design, the ` chars. are retained as part of the verbatim '...' string, to be interpreted later during wildcard-pattern processing.
See the "Background information" section below, which discusses when PowerShell's tab completion assumes that a filename argument isn't just a literal name but a wildcard pattern, as is the case here.

What is the correct way to deal with such parameters?

To prevent the escaping, make sure that your script's (first positional) parameter is named
-LiteralPath (parameter variable name $LiteralPath)
.

A quick demonstration with a function:

# Tab-completing an argument that binds to a -LiteralPath parameter deactivates
# escaping.
function foo { param($LiteralPath) $LiteralPath }; foo .\file[bla].dot

This requirement is unfortunate, since the name -LiteralPath isn't always appropriate - and, unfortunately, using a different name in combination with an [Alias('LiteralPath') attribute does not work.

If you need to use a different parameter name, you can roll your own, unescaped file-name tab completion, via an [ArgumentCompleterAttribute()] attribute:

function foo {
 param(
  [ArgumentCompleter({ 
      param($unused1, $unused2, $wordToComplete)
      (Get-Item ('{0}*' -f ($wordToComplete -replace '[][]', '``$&'))).Name
  })]
  $SomeParam
 ) 
 $SomeParam  # echo for diagnostic purposes
}
foo .\file[bla].dot

Note:

  • This simplistic implementation only works with files in the current directory, but can be adapted to work with all paths.

  • Note the use of verbatim `` in the replacement string, which is a workaround for a bug described in GitHub issue #7999.


An alternative is to not try to fix the tab-completion behavior and instead unescape the argument received, which should work even when the argument isn't escaped:

function foo { param($SomeParam) [WildcardPattern]::Unescape($SomeParam) }
# The escaped path is now unescaped inside the function.
foo '.\file`[bla`].dot'

Note: In the unlikely event that you have file names that contain literal `[ and `] sequences and the user types or pastes such a literal name, the approach would malfunction. In fact, even tab-completion with escaping fails in this case, as of v7.1.0; e.g. verbatim `[ turns to escaped ``[, even though it should be ```[


Caveat:

  • If you do use file paths that contain [ and ] unescaped, you have to make sure that you use the -LiteralPath parameter when you pass these paths to file-processing cmdlets such as Get-Item, Get-ChildItem or Get-Content later.

Background information:

For any parameter name other than -LiteralPath, notably -Path, PowerShell assumes that the parameter is designed to accept wildcard expressions and therefore escapes wildcard metacharacters that occur verbatim in file names with `; since * and ? cannot be part of file names (at least on Windows), this effectively applies to [ and ].

Note that ` is not a string-interpolation escape character in this case: given that a verbatim single-quoted string ('...') is used in the completion, the ` characters are preserved. However, if you use such a string in a context where a wildcard expression is expected, it is the wildcard engine that then recognizes ` as its escape character.

Conversely, this means that if you use an expandable (interpolating) double-quoted string ("..."), you must use `` to escape wildcard metacharacters - and tab-completion indeed does that if you start your argument with "


As of v7.1, PowerShell's tab-completion behavior with respect to file names is somewhat unfortunate:

It is reasonable default behavior to perform file-name completion for any parameters whose completion behavior isn't implied by their type (e.g., an [enum]-derived type) or where no custom completion logic is specified (such as via [ArgumentCompleterAttribute()] or [ValidateSet()] attributes).

However:

  • It is questionable whether it makes sense to assume that all such parameters by default support wildcard expressions, except for the seemingly hard-coded -LiteralPath exception.

    • Ideally, wildcard support should solely be inferred from the presence of a [SupportsWildcards()] on the target parameter - see GitHub suggestion #14149

    • Presumably, the reason that wildcard support is assumed by default is that the file-processing cmdlets' first positional parameter is the wildcard-based -Path parameter (e.g. Get-ChildItem *.txt), for which escaping of verbatim [ and ] is needed.

      • While the escaping makes it easier to pass arguments to such cmdlets positionally, this also means that if an argument is not obtained by tab-completion - such as by typing or pasting the name - escaping must be performed too, which may be unexpected.

      • Conversely, code that expects unescaped paths and uses -LiteralPath with file-processing cmdlets breaks with an escaped path resulting from tab completion.

  • It is pointless to perform file-name completion on type-constrained parameters whose type is something other than [string] or [System.IO.FileInfo] / [System.IO.DirectoryInfo] (or arrays thereof).

    • For instance, function foo { param([int] $i) } currently unexpectedly also tab-completes file names, even though that makes no sense (and breaks the call, if it is attempted).
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
mklement0
  • 382,024
  • 64
  • 607
  • 775
0

Looks like it was fixed in powershell 7. In powershell 5 you'd have to use -literalpath.

dir '.\file`[1`].txt'

    Directory: C:\Users\js\foo

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---          11/17/2020  9:04 AM              5 file[1].txt
js2010
  • 23,033
  • 6
  • 64
  • 66
  • It seems to work somehow for built-in cmdlets, but I am talking about my own cmdlet. – Richard W Nov 17 '20 at 15:35
  • That was my mistake: I naively thought `Get-Item` and `Get-ChildItem` behave the same, but they don't, so `Get-ChildItem` with an escaped path is indeed still broken in Windows PowerShell (v5.1), and probably won't get fixed (but, as you state, it has been fixed in PowerShell [Core]). In Windows PowerShell you have to _double_ the backticks to make it work with `Get-ChildItem`, which is a problem that also still affects PowerShell [Core] in _other_ areas - see [GitHub issue #7999](https://github.com/PowerShell/PowerShell/issues/7999) – mklement0 Nov 17 '20 at 19:26
  • That said, this problem is incidental to the question at hand: Richard wants the file name to tab-complete _unescaped_, for binding to a custom script's parameter. – mklement0 Nov 17 '20 at 19:28