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

Posted

技术标签:

【中文标题】当带有单引号的文件通过脚本传递时,Powershell 脚本失败。备用批处理文件也因 & 和 ! 而失败人物【英文标题】:Powershell script is failing when files with a single quote are passed through script. Alternate batch file is also failing with & and ! characters 【发布时间】:2022-01-16 09:22:43 【问题描述】:

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

我有一个简单的包装脚本,名为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 只是我可以使用的一个函数,它可以将路径/文件名复制到剪贴板。这与问题无关,只是假设它按照它所说的去做。

调用包装器的方式是通过 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 的 single 实例。如果我不使用该程序并在选择多个文件的情况下运行我的命令,它将为每个选定的文件启动多个 PowerShell 实例。创建自己的 shell 扩展和实现 IPC 等是下一个最好的事情。

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

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

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

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


    创建一个批处理文件并将我的注册表命令重定向到它。 将 SingleInstanceAccumulator 分隔符更改为“/”(所有文件将用正斜杠分隔。) 将有问题的单引号替换为两个单引号。 用单引号替换“/”分隔符。 最后将整个参数列表传回 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

连接好之后,我开始庆祝……直到我找到一个带有 & 或感叹号 (!) 的文件。一切再次崩溃。关于转义 & 和 !字符,但没有任何建议对我有用。

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

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

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

结论:我很抱歉这篇文章的小说。我试图完成的工作有很多细微差别,需要解释一下。我的问题如下:

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

任何帮助都将不胜感激。

编辑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!

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

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

如果有人知道以更优雅的方式完成同一件事的方法,我仍然非常愿意接受其他解决方案。

【问题讨论】:

我完全不确定您如何期望某些东西可以按照预期的方式正确使用 "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" 中的双引号。您有嵌套的双引号,我认为这需要转义才能理解字符串位置。 他们不需要转义。 "C:\Tools\scripts\BIN\SingleInstanceAccumulator.exe" -q:' "-c:pwsh -noprofile -windowstyle hidden -Command "C:\Tools\scripts\VSYSCopyPathToClipboard.ps1" -Path $files" "%1" 工作得很好,除了带有单引号的文件名或目录。这是一个快速的模型,将调用分解为多个块:i.imgur.com/Wi2L2Ly.png ` 如果你说它有效,那很好,我只是说我看到了一个明确定义的单双引号元素"-c:pwsh -noprofile -windowstyle hidden -Command " Unicode 标准名称 " 是引号,' 是撇号。我们应该弃用“双引号”和“单引号”术语。只是说“引用它”是模棱两可的。 @lit 公平点。我会用我的术语不那么模棱两可。 【参考方案1】:

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

使用辅助。 cmd.exe 调用将$files 宏原样和管道 回显到pwsh.exe;使SingleInstanceAccumulator.exe double-quote 成为单独的路径(默认情况下),但使用 no 分隔符 (d:"") 以便在表格"<path 1>""<path 2>""...

使pwsh.exe 通过自动$input 变量引用管道输入,并通过" 将其拆分为单个路径的数组(删除作为-ne '' 拆分副作用的空元素)。在this related answer 中更详细地讨论了通过管道 (stdin) 提供路径的必要性。

生成的数组可以安全地传递给您的脚本。

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

注意:您可能不这样做就逃脱了;但是,这将导致 空白规范化,这(尽管不太可能)会将名为 foo bar.txt 的文件更改为 foo bar.txt(多个空格的运行被规范化为单个空格)。

转义 " 字符作为 \" 是 PowerShell 的 -Command (-c) CLI 参数将它们作为逐字的一部分在初始命令行解析之后看到要执行的 PowerShell 代码,在此期间任何 未转义 " 字符都将被剥离。

因此,存储在注册表中的第一个命令应该是(类似地调整第二个命令;注意echo $files 和随后的| 之间必须有没有空格):

"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"

注意

如果您修改脚本以接受路径作为单个参数而不是数组通过-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 空格-分隔(默认情况下双引号)路径。由于-File 传递传递参数逐字,因此无需担心路径由哪些字符组成。


独立的 PowerShell 示例代码

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

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

以下有助于排除故障:

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

-windowstyle hidden 被省略以保持 PowerShell 命令可见的控制台窗口,并添加了-noexit 以便在命令执行完成后保持窗口打开。

先决条件:

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

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

注意:

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

PowerShell 7.2 的可悲现实是,在传递给外部程序的参数中需要额外的手动\-转义嵌入的" 字符 >。这可能在未来的版本中得到修复,可能需要选择加入。有关详细信息,请参阅this answer。下面的代码通过-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."

现在您可以在文件资源管理器中右键单击多个文件/文件夹并选择Copy Paths to Clipboard,以便通过一次操作将所有选定项目的完整路径复制到剪贴板。


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

【讨论】:

非常感谢您。我将测试您提供的所有代码。感谢您添加了一个将路径复制到剪贴板的示例。但是,我对此有自己的功能,因为我在其内置的“复制路径”上下文菜单项中执行 Windows 默认情况下不执行的附加数字和字母排序。看到这个:github.com/visusys/VSYSFileOps/blob/main/Public/… 希望您提供的注册表语法将结束这个逃避的噩梦。我只想毫无问题地将多个文件传递给我的脚本。为什么这么难? :) @Jay,我现在也将这个答案更新为(希望)完全可靠。请注意有关使用 -File 参数的新部分,这大大简化了解决方案 - 但需要您相应地修改脚本。

以上是关于当带有单引号的文件通过脚本传递时,Powershell 脚本失败。备用批处理文件也因 & 和 ! 而失败人物的主要内容,如果未能解决你的问题,请参考以下文章

shell入门练习

在带有 innerHTML 的 Javascript 中使用引号

将单引号和双引号传递给python脚本

shell中的参数引用

带有单引号的 MySQL 语句的 PHP 脚本

在 VB 脚本中转义单引号 >> Java 脚本