当通过脚本传递带有单引号的文件时,Powershell 脚本失败。备用批处理文件也因 & 和 ! 而失败人物

Powershell script is failing when files with a single quote are passed through script. Alternate batch file is also failing with & and ! characters

这是一个看似复杂的问题,但我会尽力解释这个问题。

我有一个简单的包装脚本,如下所示 VSYSCopyPathToClipboard.ps1:

param (
    [Parameter(Mandatory,Position = 0)]
    [String[]]
    $Path,

    [Parameter(Mandatory=$false)]
    [Switch]
    $FilenamesOnly,

    [Parameter(Mandatory=$false)]
    [Switch]
    $Quotes
)

if($FilenamesOnly){
    Copy-PathToClipboard -Path $Path -FilenamesOnly
}else{
    Copy-PathToClipboard -Path $Path
}

Copy-PathToClipboard 只是我可用的一个函数,可以将 paths/filenames 复制到剪贴板。这与问题无关,假设它按照它说的做。

包装器的调用方式是通过 Windows 右键单击​​上下文菜单。这涉及在此处创建密钥:HKEY_CLASSES_ROOT\AllFilesystemObjects\shell\.

我的看起来像这样:

命令如下:

"C:\Tools\scripts\BIN\SingleInstanceAccumulator.exe" -q:' "-c:pwsh -noprofile -windowstyle hidden -Command "C:\Tools\scripts\VSYSCopyPathToClipboard.ps1" -Path $files" "%1"

“复制为文件名”也类似:

"C:\Tools\scripts\BIN\SingleInstanceAccumulator.exe" -q:' "-c:pwsh -noprofile -windowstyle hidden -Command "C:\Tools\scripts\VSYSCopyPathToClipboard.ps1" -FilenamesOnly -Path $files" "%1"

我在这里使用一个名为 SingleInstanceAccumulator 的工具。这允许我将多个选定的文件传递给 PowerShell 的 单个 实例。如果我没有使用这个程序并且 运行 我的命令选择了多个文件,它将为每个选择的文件启动多个 PowerShell 实例。这是仅次于创建您自己的 shell 扩展和实施 IPC 等的最佳选择。

这一直很好用,直到今天我遇到一个文件名中有单引号的文件 (I.E.testing'video.mov),整个脚本都失败了。它失败了,因为我在 SingleInstanceAccumulator 中使用的分隔符也是单引号,而 PowerShell 看不到匹配的引号...因此出错。

如果我的变量是静态的,我可以通过将有问题的单引号加倍来解决这个问题,但由于我的参数是文件,除了重命名文件本身之外,我没有机会转义单引号......这是一个非-解决方案。

所以现在我不知道如何处理这个。

我第一次尝试解决这个问题是这样的:


  1. 创建一个批处理文件并将我的注册表命令重定向到它。
  2. 将 SingleInstanceAccumulator 分隔符更改为“/”(所有文件将由正斜杠分隔。)
  3. 将有问题的单引号替换为两个单引号。
  4. 将“/”分隔符替换为单引号。
  5. 最后将整个参数列表传回 Powershell。

这张图片展示了上述过程的样子:

这是批处理文件的代码:

@echo off
setlocal EnableDelayedExpansion

:: This script is needed to escape filenames that have 
:: a single quote ('). It's replaced with double single
:: quotes so the filenames don't choke powershell
:: echo %cmdcmdline%

set "fullpath=%*"

echo Before 
echo !fullpath!
echo ""

echo After
set fullpath=%fullpath:'=''%
set fullpath=%fullpath:/='%
echo !fullpath!

:: pwsh.exe -noprofile -windowstyle hidden -command "%~dpn0.ps1 -Path !fullpath!

pause

一旦我连接好了,我就开始庆祝......直到我找到一个带有符号 (&) 或感叹号 (!) 的文件。一切再次土崩瓦解。我做了一大堆 google-fu 关于转义 & 和 !字符,但没有任何建议对我有用。

如果我将 'C:\Users\futur\Desktop\Testing\Video Files\MOV Batch\testing&video.mov' 传递到我的批处理文件中,我会返回 'C:\Users\futur\Desktop\Testing\Video Files\MOV Batch\testing

它会在与符号的确切位置截断字符串。

我觉得必须有办法解决这个问题,而且我遗漏了一些愚蠢的东西。如果我回显 %cmdcmdline% 它会显示完整的命令行 &,所以它可以通过该变量以某种方式使用。

总结:对不起post的小说。我试图完成的事情有很多细微差别需要解释。我的问题如下:

  1. 我能否仅使用 Powershell 并以某种方式预转义单引号来完成此操作?
  2. 我可以使用批处理文件完成此操作,并以某种方式预转义 &!(以及任何其他会导致失败的特殊字符)吗?

任何帮助都将不胜感激。

编辑 1:


因此,我以最可怕、最骇人听闻的方式解决了我的问题。但是因为它太可怕了,我觉得这样做很可怕,所以我仍在寻找合适的解决方案。

基本上,回顾一下,当我做这些变量赋值时:

set "args=%*"
set "args=!%*!"
echo !args!

&! 字符仍然会破坏内容,而且我没有得到我的文件的完整枚举。 & 的文件被截断等

但我注意到了:

set "args=!cmdcmdline!"
echo !args!

我得到了保留所有特殊字符的完整命令行调用:

C:\WINDOWS\system32\cmd.exe /c ""C:\Tools\scripts\VSYSCopyPathToClipboardTest.bat" /C:\Users\futur\Desktop\Testing\Video Files\MOV Batch\KylieCan't.mov/,/C:\Users\futur\Desktop\Testing\Video Files\MOV Batch\The !Rodinians - Future Forest !Fantasy - FFF Trailer.mov/,/C:\Users\futur\Desktop\Testing\Video Files\MOV Batch\Yelle - Je Veu&x Te Voir.mov/,/C:\Users\futur\Desktop\Testing\Video Files\MOV Batch\Erik&Truffaz.mov/,/C:\Users\futur\Desktop\Testing\Video Files\MOV Batch\my_file'name.mov/,/C:\Users\futur\Desktop\Testing\Video Files\MOV Batch\testing&video.mov/"

所以我所做的只是去掉字符串的初始 C:\WINDOWS\system32\cmd.exe /c ""C:\Tools\scripts\VSYSCopyPathToClipboardTest.bat" 部分:

@echo off
setlocal enableDelayedExpansion

set "args=!cmdcmdline!"
set args=!args:C:\WINDOWS\system32\cmd.exe=!
set args=!args: /c ""C:\Tools\scripts\VSYSCopyPathToClipboard.bat" =!
set args=!args:'=''!
set args=!args:/='!
set args=!args:~0,-1!

echo !args!

pwsh.exe -noprofile -noexit -command "%~dpn0.ps1 -Path !args!

而且...它完美无缺。它可以处理我扔给它的任何疯狂角色,而无需逃避任何事情。我知道这完全是解决这个问题的最堕落的垃圾方式,但在任何地方都找不到解决方案会让我采取绝望的措施。 :)

我可能会让字符串删除更通用一些,因为如果我更改文件名,它实际上会中断。

如果有人知道以更优雅的方式完成同一件事的方法,我仍然对其他解决方案持非常开放的态度。

  • 基于 PowerShell 的 -Command (-c)完全可靠的解决方案CLI 参数可以处理路径中的 ' 个字符以及 $` 个字符需要相当复杂的解决方法,不幸的是:[1]

    • 使用辅助。 cmd.exe 调用按原样回显 $files 宏,pipe 回显到 pwsh.exe;使 SingleInstanceAccumulator.exe 双引号 单独的路径(默认情况下),但使用 no 分隔符(d:"" ) 为了有效地输出 "<path 1>""<path 2>""...

      形式的字符串
    • 使 pwsh.exe 通过自动 $input 变量引用管道输入,并通过 " 将其拆分为单个路径的数组(删除空元素用 -ne '' 拆分的副作用)。 .

      中更详细地讨论了通过管道 (stdin) 提供路径 的必要性
    • 生成的数组可以安全地传递给您的脚本。

  • 此外,将传递给 pwsh.exe in \"...\" 的整个 -Command (-c) 参数包含在 "-c:..." 参数中。

    • 注意:您可能不这样做就逃脱;然而,这将导致 whitespace 规范化 ,这(尽管不太可能)会将名为 foo bar.txt 的文件更改为 foo bar.txt( 运行 的多个 space 被归一化为单个 space).

    • 转义 " 个字符,因为 \" 对于 PowerShell 的 -Command (-c) CLI 参数来对待它们 逐字 ,作为要执行的 PowerShell 代码的一部分 初始命令行解析之后,在此期间任何 未转义的 " 字符都被删除。

因此,存储在注册表中的第一个命令应该是(类推第二个;注意[=40=之间不能有没有space ] 和随后的 |):

"C:\Tools\scripts\BIN\SingleInstanceAccumulator.exe" -d:"" "-c:cmd /c echo $files| pwsh.exe -noprofile -c \"& 'C:\Tools\scripts\VSYSCopyPathToClipboard.ps1' -Path ($input -split '\\"' -ne '')\"" "%1"

:

如果您修改脚本以接受路径作为 单个 参数而不是作为 数组 ,那么 much通过 -File CLI 参数 (而不是 -Command (-c))的更简单的解决方案是可能的。这可以像使用 [Parameter(ValueFromRemainingArguments)] 修饰 $Path 参数声明一样简单,然后调用脚本 而无需 显式命名目标参数 (-Path):

"C:\Tools\scripts\BIN\SingleInstanceAccumulator.exe" -d:" " "-c:pwsh.exe -noprofile -File \"C:\Tools\scripts\VSYSCopyPathToClipboard.ps1\" $files" "%1"

请注意使用 -d:" " 使 SingleInstanceAccumulator.exe space 分隔(默认情况下双引号)路径。由于 -File 逐字传递传递参数 ,因此无需担心路径由哪些字符组成。


独立的 PowerShell 示例代码:

以下代码为所有文件系统对象(驱动器除外)定义了一个 Copy Paths to Clipboard 快捷菜单命令:

  • 不涉及单独的 .ps1 脚本;相反,传递给 -Command / -c 的代码直接执行所需的操作(复制传递给剪贴板的路径)。

  • 以下内容有助于故障排除:

    • 打印调用 PowerShell 的完整命令行 ([Environment]::CommandLine),以及传递的路径列表 ($file)

    • 省略
    • -windowstyle hidden 以保持控制台 window 在其中可见 PowerShell 命令并添加 -noexit 以保持 window命令执行完成后打开。

先决条件:

  • 使用 Visual Studio 下载并构建 SingleInstanceAccumulator project(可以使用 .NET SDK,但需要额外的工作)。

  • 将生成的 SingleInstanceAccumulator.exe 文件放在 $env:Path 环境变量中列出的目录之一中。或者,指定下面可执行文件的完整路径。

注:

  • reg.exe 使用 \ 作为其转义字符,这意味着应该成为存储在注册表中的字符串的一部分的 \ 字符必须是 转义,如\.

  • PowerShell 7.2 的悲惨现实是 额外的手动\-转义嵌入的 " 字符在传递给 外部程序 的参数中是必需的。此 可能 在未来的版本中得到修复, 可能 需要选择加入。有关详细信息,请参阅 。下面的代码通过 -replace '"', '\"' 操作执行此操作,如果在未来的 PowerShell 版本中不再需要它,可以轻松将其删除。

# RUN WITH ELEVATION (AS ADMIN).

# Determine the full path of SingleInstanceAccumulator.exe:
# Note: If it isn't in $env:PATH, specify its full path instead.
$singleInstanceAccumulatorExe = (Get-Command -ErrorAction Stop SingleInstanceAccumulator.exe).Path

# The name of the shortcut-menu command to create for all file-system objects.
$menuCommandName = 'Copy Paths To Clipboard'

# Create the menu command registry key.
$null = reg.exe add "HKEY_CLASSES_ROOT\AllFilesystemObjects\shell$menuCommandName" /f /v "MultiSelectModel" /d "Player"
if ($LASTEXITCODE) { throw }

# Define the command line for it.
# To use *Windows PowerShell* instead, replace "pwsh.exe" with "powershell.exe"
# SEE NOTES ABOVE.
$null = reg.exe add "HKEY_CLASSES_ROOT\AllFilesystemObjects\shell$menuCommandName\command" /f /ve /t REG_EXPAND_SZ /d (@"
"$singleInstanceAccumulatorExe" -d:"" "-c:cmd /c echo `$files| pwsh.exe -noexit -noprofile -c \"[Environment]::CommandLine; `$paths = `$input -split [char] 34 -ne ''; `$paths; Set-Clipboard `$paths\"" "%1"
"@ -replace '"', '\"')

if ($LASTEXITCODE) { throw }

Write-Verbose -Verbose "Shortcut menu command '$menuCommandName' successfully set up."

现在您可以在文件资源管理器中右键单击多个 files/folders 和 select Copy Paths to Clipboard 以将所有 selected 项目的完整路径复制到单次操作剪贴板。


[1] 另一种方法是使用 -f option,这会导致 SingleInstanceAccumulator.exe 将所有文件路径逐行写入辅助 文本file,然后将 $files 扩展为该文件的完整路径。但是,这需要相应地设计目标脚本,清理辅助文本文件是他们的责任。