How to make powershell inline script path recognise spaces? - powershell

The boss changed TFS servers and added a space in the path: D:\TFS Agent\xxx and it's causing my powershell script to fail upon release.
We have build/release automatic integration with TFS and I have an inline powershell task to read a file and convert it to JSON then execute some SQL. I had this working until this morning.
The problem is that the agent path is a system variable in TFS: $(System.DefaultWorkingDirectory) and I'm not sure how to handle the space in the path.
I've tried this:
# Begin inline script
Param(
[string]$path,
[string]$db,
[string]$schema,
[string]$envName
)
# Create module
$formattedPath = $path -replace " ", "` ";
$conv = Get-Content -Raw -Path "$formattedPath" | ConvertFrom-Json;
But all I get is D:\TFS. The path looks like this before the replace:
D:\TFS Agent\_work\r3\a\xxx
I can't for the life of me figure out how to replace the space with a tick mark or how to ignore spaces. I'm very new to powershell so this may just be some simple thing, but my google-fu is not strong today. Any help would be appreciated. Thanks!

It seems you are pass $(System.DefaultWorkingDirectory) with the variable path as below:
-path $(System.DefaultWorkingDirectory)
While, if $(System.DefaultWorkingDirectory) contains spaces (such as D:\TFS Agent\_work\r3\a), it will show divided the values into different lines by spaces (such as D:\TFS Agent\_work\r3\a will show in two lines with value D:\TFS and Agent\_work\r3\a). So the variable $path with the value in the first line (like D:\TFS).
To solve the problem, you just need to add double quotes for $(System.DefaultWorkingDirectory). So just change the argument in PowerShell task as:
-path "$(System.DefaultWorkingDirectory)"

Related

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

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:
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 SingleInstanceAccumulator. This allows me to pass multiple selected files to a single instance of PowerShell. If I didn't use this program and ran my command with multiple files selected it would launch multiple instances of PowerShell for each file selected. It's the next best thing to creating your own shell extension and implementing IPC etc.
This has been working great until today when I encountered a file with a single quote in its filename (I.E.testing'video.mov) and the entire script failed. It's failing because the delimiter I'm using with SingleInstanceAccumulator is also a single quote and PowerShell sees no matching quote... thus errors out.
I could fix this if my variables were static by just doubling up the offending single quote, but since my parameters are files I have no opportunity to escape the single quote beyond renaming the file itself ... which is a non-solution.
So now I have no clue how to handle this.
My first try at solving the problem was as such:
Create a batch file and redirect my registry command to it.
Change the SingleInstanceAccumulator delimiter to '/' (All files will be separated by a forward slash.)
Replace the offending single quote to two single quotes.
Replace the '/' delimiters with single quotes.
Finally pass the whole argument list back to Powershell.
This image demonstrates how the above process looks:
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:
Can I accomplish this with Powershell only and somehow pre-escape single quotes?
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.
A fully robust solution based on PowerShell's -Command (-c) CLI parameter that can handle ' characters in paths as well as $ and ` ones requires a fairly elaborate workaround, unfortunately:[1]
Use an aux. cmd.exe call that echoes the $files macro as-is and pipe that to pwsh.exe; make SingleInstanceAccumulator.exe double-quote the individual paths (as it does by default), but use no delimiter (d:"") in order to effectively output a string in the form "<path 1>""<path 2>""...
Make pwsh.exe reference the piped input via the automatic $input variable and split it into an array of individual paths by " (removing empty elements that are a side effect of splitting with -ne ''). The necessity for providing the paths via the pipeline (stdin) is discussed in more detail in this related answer.
The resulting array can safely be passed to your scripts.
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).
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; note that there must be no space between the echo $files and the subsequent |):
"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"
Note:
If you modified your scripts to accept the paths as individual arguments rather than as an array, a much simpler solution via the -File CLI parameter (rather than -Command (-c)) is possible. This could be as simple as decorating the $Path parameter declaration with [Parameter(ValueFromRemainingArguments)] and then invoking the script without naming the target parameter explicitly (-Path):
"C:\Tools\scripts\BIN\SingleInstanceAccumulator.exe" -d:" " "-c:pwsh.exe -noprofile -File \"C:\Tools\scripts\VSYSCopyPathToClipboard.ps1\" $files" "%1"
Note the use of -d:" " to make SingleInstanceAccumulator.exe space-separate the (double-quoted by default) paths. Since -File passes the pass-through arguments verbatim, there is no concern about what characters the paths are composed of.
Self-contained PowerShell sample code:
The following code defines a Copy Paths to Clipboard shortcut-menu command for all file-system objects (except drives):
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.
# 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."
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.

How do i use Get-clipboard output in a powershell script?

i'm trying to use text that is inside the clipboard inside a powershell script. So what the purpose for the script, i want to be able to copy a file directory and then run the script so it uses the copied directory as a automatic destination. So my idea was to do something like this:
Copy-Item C:\Users\gif.gif -Destination "Copied Directory"
I'm very new to powershell scripting, so an explenation of what is going on would be nice but not needed. I origionlly thought that this could work but nope XD, this should have been a simple project but yeah it wasn't meant to be easy.
Copy-Item C:\Users\J.J\Documents\TouchPortal/Make_30fps_gif.bat -Destination | Get-Clipboard
Would love to get some help with this, and thank you in advance!
To complement Steven's helpful answer:
In Windows PowerShell only, you can use Get-Clipboard's -Format parameter to request clipboard data other than text.
In PowerShell [Core, v6+], this is no longer supported, and text is indeed the only data type supported by Get-Clipboard.
In Windows PowerShell, if you've used File Explorer to copy a directory to the clipboard (using the regular Copy shortcut-menu command or the Ctrl+C keyboard shortcut), you can access it as System.IO.DirectoryInfo instance by passing -Format FileDropList to Get-Clipboard.
Note: -Format FileDropList returns a collection of file-system-info objects, so as to also support multiple files/directories having been copied to the clipboard; thanks to member-access enumeration, however, you can treat this collection like a single object - assuming truly only one file or directory was copied.
# Note: Windows PowerShell only.
# The .FullName property returns the full directory path.
Copy-Item C:\Users\gif.gif -Destination (Get-Clipboard -Format FileDropList).FullName
In PowerShell [Core, v6+] you'll indeed have to use the Shift+right-click method and select Copy as path from the shortcut menu, in order to ensure that a file/directory is copied as a (double-quoted, full) path string, as shown in Steven's answer.
In PowerShell core including versions 6 & 7 Get-Clipboard only works with text. If you use it after copying a folder it will return null.
In PowerShell 5.1 (Windows PowerShell) you can use the -Format parameter with Get-Clipboard
See mklement0's answer for a better description and example using -Format.
If you need to use the newer versions, you can use the shift + context menu choice > Copy as Path to get the folder's string path on to the clipboard, but that will quote the path. The quoted path will then be rejected by Copy-Item.
However, you could quickly replace the quotes like below.
Copy-Item 'C:\temp\BaseFile.txt' -Destination (Get-Clipboard).Replace('"',"")
Caution though, this seems hazardous and I wouldn't advise it. I use Get-Clipboard all the time to get data into a console session and can attest that it's too easy to make mistakes. The clipboard is so transient and it's use so ubiquitous that even if you make this work it's bound to burn you at some point.
Maybe you can elaborate on what you're trying to do and why. Then we can brainstorm the best approach.

How to access file paths in PowerShell containing special characters

I am calling PowerShell from within a Java application (through the Windows command prompt) to read various file attributes.
For example,
powershell (Get-Item 'C:\Users\erlpm\Desktop\Temp\s p a c e s.txt').creationTime
I am enclosing the file path in single quotes, because it may contain spaces.
It worked fine, until I encountered a file path containing square brackets, which seem to be interpreted as a wildcard character. I managed to solve it by adding the -literalPath parameter:
powershell (Get-Item -literalpath 'C:\Users\erlpm\Desktop\Temp\brackets[].txt').creationTime
So far, so good... But file paths may also contain single quotes, dollar signs, ampersands, etc., and all these characters seem to have a specific function in PowerShell for which the -literalPath parameter does not seem to work.
I tried enclosing the path with double quotes or escaping with the `character, but that did not solve my problem either :-(
Any suggestions on how to pass a file path to PowerShell which may contain spaces, single quotes, square brackets, ampersands, dollar signs, etc.?
Someone here already showed me how to get it working from within PowerShell, but somehow the answer has been removed(?).
Anyway, I did create a file called $ & ' [].txt.
This works form within PowerShell (needed to escape the &):
Get-Item -LiteralPath "C:\Users\erlpm\Desktop\Temp\`$ & ' [].txt"
Directory: C:\Users\erlpm\Desktop\Temp
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 2012-08-23 14:22 0 $ & ' [].txt
But when I execute the same PowerShell command through the Windows command prompt, ...
powershell Get-Item -LiteralPath "C:\Users\erlpm\Desktop\Temp\`$ & ' [].txt"
... I get this error:
Ampersand not allowed. The & operator is reserved for future use; use "&" to pass ampersand as a string.
At line:1 char:55 \
Get-Item -LiteralPath C:\Users\erlpm\Desktop\Temp`$ & <<<< ' [].txt \
CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException \
FullyQualifiedErrorId : AmpersandNotAllowed
Using the -command parameter and putting the PowerShell command between {} gives exactly the same error message ...
powershell -command {Get-Item -LiteralPath "C:\Users\erlpm\Desktop\Temp\`$ & ' [].txt"}
This is really a question about cmd.exe string escaping, then. Combining cmd and PowerShell string escaping is a total nightmare. After quite a few attempts, I got your specific example to work:
powershell.exe -nologo -noprofile -command ^&{ dir -LiteralPath ^"""".\`$ & ' [].txt"" }
Directory: C:\Users\latkin
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 8/23/2012 8:46 AM 0 $ & ' [].txt
Totally intuitive, right?
You can spend 15 minutes wrestling with escape sequences every time, or you can try some other approaches.
Put the file name in a text file, and read it out of there.
powershell.exe -command "&{dir -literal (gc .\filename.txt) }"
or
Use a script
powershell.exe -file .\ProcessFiles.ps1 # In processfiles.ps1 you are in fully powershell environment, so escaping is easier
or
Use -EncodedCommand
See powershell.exe -? (last item shown) or http://dmitrysotnikov.wordpress.com/2011/07/06/passing-parameters-to-encodedcommand/
If you can write the path to a temporary .txt file, the following works OK:
(Get-Item -literalpath (gc 'C:\Temp\path.txt')).creationTime
The file # C:\Temp\path.txt contains the path with special characters in it, other than this I think you would have to escape each special character on a per path basis.
In addition to the hack above, it appears PowerShell V3 may help out here with the addition of a new 'magic parameter' language feature. Specifically, see the section headed 'Easier Reuse of Command Lines From Cmd.exe' here and because I hate link-rot, here it is, shamelessly reproduced below:
Easier Reuse of Command Lines From Cmd.exe
The web is full of command lines written for Cmd.exe. These commands
lines work often enough in PowerShell, but when they include certain
characters, e.g. a semicolon (;) a dollar sign ($), or curly braces,
you have to make some changes, probably adding some quotes. This
seemed to be the source of many minor headaches.
To help address this scenario, we added a new way to “escape” the
parsing of command lines. If you use a magic parameter --%, we stop
our normal parsing of your command line and switch to something much
simpler. We don’t match quotes. We don’t stop at semicolon. We
don’t expand PowerShell variables. We do expand environment variables
if you use Cmd.exe syntax (e.g. %TEMP%). Other than that, the
arguments up to the end of the line (or pipe, if you are piping) are
passed as is. Here is an example:
echoargs.exe --% %USERNAME%,this=$something{weird}
Arg 0 is <jason,this=$something{weird}>
This thread is 5 years old so maybe times have changed, but the current answer that worked for me was to use the backtick (`) symbol to escape the special character.
In my case, it was a dollar sign in a directory path that was failing. By putting a backtick before the dollar sign, everything worked.
Before:
$dir = "C:\folder$name\dir" # failed
After:
$dir = "C:\folder`$name\dir" # succeeded

Dot sourcing a PowerShell script not ending with .ps1

From a PowerShell program, I can "dot source" another PowerShell program. i.e I can execute it as if it were written inside the first one.
Example:
Write-Host 'before'
. MyOtherProgram.ps1
Write-Host 'after'
MyOtherProgram in 'included' inside the main program, exactly as if its content had been copy/pasted.
The problem is: I can only dot source a filename finishing with .ps1
I can't with MyOtherProgram.lib or MyOtherProgram.whatever
Anyone have a method to dot source a PowerShell script not ending with .ps1 ?
Another way would be using Invoke-Expression:
$code = Get-Content ./MyOtherProgram.lib | Out-String
Invoke-Expression $code
I'm not aware if this is compiled into PowerShell or if it's configurable but one way to do it is just have your script temporarily rename it, import it and then rename it back.
Rename-Item C:\Path\MyScript.whatever C:\Path\MyScript.ps1
. C:\Path\MyScript.ps1
Rename-Item C:\Path\MyScript.ps1 C:\Path\MyScript.whatever

How do I update Windows path WITHOUT losing the original embedded environment variables

Windows 2008R2
Powershell v2.0
Original Path (as seen from Advanced System Settings/Environment Variables) is:
%SystemRoot%\system32;%SystemRoot%;%SystemRoot%\System32\Wbem;%SYSTEMROOT%\System32\WindowsPowerShell\v1.0\
From Powershell I run:
[Environment]::SetEnvironmentVariable("PATH", "$($env:path;C:\Temp", "Machine")
or
[Environment]::SetEnvironmentVariable("PATH", "$($([Environment]::GetEnvironmentVariable('PATH', 'MACHINE')));C:\Temp", "Machine")
Now my path (as seen from Advanced System Settings/Environment Variables) is:
C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Temp
Is there a way to retrieve the existing path WITHOUT evaluating it so that I can retain the existing environment variables embedded in the original path?
Kind of medieval, but seems to work:
(((reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment") |
select-string "\s+path\s+REG_EXPAND_SZ").line -split " ")[3]
I think the problem is how PS is getting the environment variable;
This S/O post might contain the answer for you (hopefully!):
PowerShell: Get 'tmp' environment variable raw value
Try this (after converting to PS):
string originalPath = Environment.GetEnvironmentVariable("PATH");
string path = originalPath + ";" + "NEW_PATH_BIT";
Environment.SetEnvironmentVariable("PATH", path);
I'm not sure there is - I tried this:
PS C:\> $string1 = " World"
PS C:\> $string2 = "Hello$string1"
PS C:\> $string2
Hello World
to see if it was related to environment variables or something else.
I guess it's the way it's compiled on the command line. All variables are evaluated before the whole expression is evaluated. So the end results doesn't know the working gone into it.