Home > Mobile >  Powershell script is failing when files with a single quote are passed through script. Alternate bat
Powershell script is failing when files with a single quote are passed through script. Alternate bat

Time:12-18

This is a deceptively complex issue, but I'll do my best to explain the problem.

I have a simple wrapper script as follows called 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 is just a function I have available that copies paths/filenames to the clipboard. It's irrelevant to the issue, just assume it does what it says.

The way the wrapper is called is through the Windows right click context menu. This involves creating a key here: HKEY_CLASSES_ROOT\AllFilesystemObjects\shell\.

Mine looks like this: Regedit

The command is as follows:

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

And similarly for the "Copy as Filename":

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

I am using a tool here called Command Parsing

This is the batch file's code:

@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

Once I got that wired up I started celebrating ... until I hit a file with an ampersand (&) or an exclamation point (!). Everything fell apart again. I did a whole bunch of google-fu with regards to escaping the & and ! characters but nothing suggested worked at all for me.

If I pass 'C:\Users\futur\Desktop\Testing\Video Files\MOV Batch\testing&video.mov' into my batch file, I get 'C:\Users\futur\Desktop\Testing\Video Files\MOV Batch\testing back.

It truncates the string at the exact position of the ampersand.

I feel like there has to be a way to solve this, and that I'm missing something stupid. If I echo %cmdcmdline% it shows the full commandline with the &, so it's available somehow with that variable.

In conclusion: I'm sorry for the novel of a post. There is a lot of nuance in what I'm trying to accomplish that needs to be explained. My questions are as follows:

  1. Can I accomplish this with Powershell only and somehow pre-escape single quotes?
  2. Can I accomplish this with a batch file, and somehow pre-escape & and ! (and any other special characters that would cause failure)?

Any help at all would be hugely appreciated.

Edit1:


So in the most hideous and hackish way possible, I managed to solve my problem. But since it's so horrible and I feel horrible for doing it I am still looking for a proper solution.

Basically, to recap, when I do either of these variable assignments:

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

& and ! characters still break things, and I don't get a full enumeration of my files. Files with & get truncated, etc.

But I noticed when I do:

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

I get the full commandline call with all special characters retained:

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

So what I did was simply strip out the initial C:\WINDOWS\system32\cmd.exe /c ""C:\Tools\scripts\VSYSCopyPathToClipboardTest.bat" part of the string:

@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!

And... it works flawlessly. It handles any crazy character I throw at it without needing to escape anything. I know It's totally the most degenerate garbage way of approaching this, but not finding a solution anywhere leads me to desperate measures. :)

I am probably going to make the string removal a bit more universal since it literally breaks if I change the filename.

I am still VERY much open to other solutions should anyone know of a way to accomplish the same thing in a more elegant way.

CodePudding user response:

  • As a verbatim part of the command line stored in the registry, use -q:\\\" instead of
    -q:'
    [1], which means that SingleInstanceAccumulator.exe will use verbatim \" as the delimiter, which PowerShell ultimately see as " (see below).

    • Using " as the ultimately effective delimiter ensures that even paths that contain ' are passed correctly as-is.
  • Also, enclose the entire -Command (-c) argument passed to pwsh.exe in \"...\" inside the "-c:..." argument.

    • Note: You may get away without doing this; however, this would result in whitespace normalization, which (however unlikely) would alter a file named, say, foo bar.txt to foo bar.txt (the run of multiple spaces was normalized to a single space).

This causes SingleInstanceAccumulator.exe to enclose the individual file paths that its placeholder $files expands to in \"...\" on the command line passed to pwsh.exe.

Escaping " characters as \" is necessary for PowerShell's -Command (-c) CLI parameter to treat them verbatim, as part of the PowerShell code to execute that is seen after initial command-line parsing, during which any unescaped " characters are stripped.

Therefore, the first command stored in the registry should be (adapt the second one analogously):

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

Self-contained PowerShell sample code:

The following code defines a Copy Paths to Clipboard shortcut-menu command for all file-system objects:

  • No separate .ps1 script is involved; instead, the code passed to -Command / -c directly performs the desired operation (copying the paths passed to the clipboard).

  • The following helps with troubleshooting:

    • The full command line with which PowerShell was invoked ([Environment]::CommandLine) is printed, as is the list of paths passed ($file)

    • -windowstyle hidden is omitted to keep the console window in which the PowerShell commands visible and -noexit is added so as to keep the window open after the command has finished executing.

Prerequisites:

  • Download and build the SingleInstanceAccumulator project using Visual Studio (using the .NET SDK is possible, but requires extra work).

  • Place the resulting SingleInstanceAccumulator.exe file in one of the directories listed in your $env:Path environment variable. Alternatively, specify the full path to the executable below.

Note:

  • reg.exe uses \ as its escape character, which means that \ characters that should become part of the string stored in the registry must be escaped, as \\.

  • The sad reality as of PowerShell 7.2 is that an extra, manual layer of \-escaping of embedded " characters is required in arguments passed to external programs. This may get fixed in a future version, which may require opt-in. See this answer for details. The code below does this by way of a -replace '"', '\"' operation, which can easily be removed if it should no longer be necessary in a future PowerShell version.

# 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" -q:\\\\\\" "-c:pwsh.exe -noexit -c \\"[Environment]::CommandLine; `$files; Set-Clipboard `$files; pause\\"" "%1"
"@ -replace '"', '\"')

if ($LASTEXITCODE) { throw }

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

Now you can right-click on multiple files/folders in File Explorer and select Copy Paths to Clipboard in order to copy the full paths of all selected items to the clipboard in a single operation.


[1] An alternative is to use the -f option instead, which causes SingleInstanceAccumulator.exe to write all file paths line by line to an auxiliary text file, and then expands $files to that file's full path. However, this requires the target scripts to be designed accordingly, and it is their responsibility to clean up the auxiliary text file.

  • Related