PowerShell execution fails due to space in directory name - powershell

I have been handed a PowerShell script that executes to install an executable which is placed in the same directory. This is packaged into SCCM (System Center Configuration Manager) for deployment. However, the deployment fails if during package deployment the temporary director where the package is pushed has a space anywhere in the file path.
For example:
C:\temp\deployment -- This will pass and the software will install.
C:\temp\deployment 1\ -- This will fail due to space between deployment and 1
C:\temporary directory\deployment\ -- This will fail due to space between temporary and directory.
Here is part of the code that I suspect is carrying out the installation:
if ($softwarename -eq "Not Installed") {
Write-Output "Installing softwarename..."
# Build a massive softwarename installation line
$tempFile = [System.IO.Path]::GetTempFileName() + ".cmd"
$installLine = "$scriptPath\softwarename.exe /s /v`"/qn INSTALLDIR=\`"C:\Program Files\directoryname\softwarename\`" AUTHTOKEN=REDACTED FULLCONSOLEADDRESS=$dest`:8413 HOSTNAME=$Env:COMPUTERNAME LOG_SOURCE_AUTO_CREATION_ENABLED=True LOG_SOURCE_AUTO_CREATION_PARAMETERS=`"`"Component1.AgentDevice=DeviceWindowsLog&Component1.Action=create&Component1.LogSourceName=$env:COMPUTERNAME&Component1.LogSourceIdentifier=$env:COMPUTERNAME&Component1.Log.Security=true&Component1.Filter.Security.Enabled=true&Component1.Filter.Security.Param=5156&Component1.Filter.Security.Type=Blacklist&Component1.Log.System=true&Component1.Log.Application=true&Component1.Log.DNS+Server=false&Component1.Log.File+Replication+Service=false&Component1.Log.Directory+Service=false&Component1.Destination.Name=$dest&Component1.RemoteMachinePollInterval=300&Component1.EventRateTuningProfile=High+Event+Rate+Server&Component1.MinLogsToProcessPerPass=1250&Component1.MaxLogsToProcessPerPass=1875`"`"`""
$installLine | Out-File -Encoding ascii -filepath $tempFile
Write-Output $tempFile
cmd.exe /c $tempFile
} else {
if ($psversiontable.psversion.major -gt 2) {
$props=ConvertFrom-StringData (Get-Content "c:\Program Files\directoryname\softwarename\config\install_config.txt" -Raw)
if ($props.ConfigurationServer -eq $dest) {
Write-Output "Configuration server is correct - no action"
} else {
Stop-Service (Get-Service -ErrorAction SilentlyContinue softwarename)
$props.ConfigurationServer=$dest
$props.StatusServer=$dest
$props.GetEnumerator() | % { "$($_.Name)=$($_.Value)" } | Out-File -Encoding ascii -filepath "c:\Program Files\directoryname\softwarename\config\install_config.txt"
del "c:\Program Files\directoryname\softwarename\config\ConfigurationServer.PEM"
Start-Service (Get-Service -ErrorAction SilentlyContinue softwarename)
Write-Output "Configuration server reset to $dest"
}
} else {
Write-Output "Powershell version does not support the reset functionality."
}
I suspect following two lines to be the cause of the problem:
$tempFile = [System.IO.Path]::GetTempFileName() + ".cmd"
$installLine = "$scriptPath\softwarename.exe /s /v`"/qn
I suspect that since execution will require exact path and a "space" in the directory path will lead to "path not found" (interestingly on my Windows 10 machine change directory [cd] command works without putting filepath with spaces in quotes - "" -- This leads me to believe I'm completely wrong, however at this point I have no other thing to look at).
Can anyone please assist on adding parameters to ensure that patch generated for .cmd file to be executed does not have a space?
I have tried to patch this by adding a batch file that copies the package to a static directory before execution. However, this is failing to deploy via SCCM.

In order to invoke a program from a batch file with a path that contains spaces, that path must be enclosed in "...":
# PowerShell string that constructs the command line to write to the batch file.
$installLine = "`"$scriptPath\softwarename.exe`" ..."
`" is how you embed a literal " in a "..." string in PowerShell.
By contrast, calling cd from a batch file with a path containing spaces also works without enclosing "...", but:
that is only possible without ambiguity because cd requires just one operand (the target dir.)
due to the inconsistency with how arguments with spaces must be handled by all other commands, supporting this was never a good idea to begin with.

Related

Why do some of my PowerShell scripts stop working when I set PowerShell as the default application?

Here are 2 of my scripts. The first shows a list of directories with more than one file. The second is just a test because I never used $PSScriptRoot before.
Get-ChildItem -Path $PSScriptRoot -Directory -Name -Recurse |
Write-Output
Where-Object { (Get-ChildItem $_ -File).Count -ge 2 }|
ForEach-Object { ".\$_"} |
Invoke-Item
# If running in the console, wait for input before closing.
if ($Host.Name -eq "ConsoleHost")
{
Write-Host "Press any key to continue..."
$Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyUp") > $null
}
Write-Output $PSScriptRoot
# If running in the console, wait for input before closing.
if ($Host.Name -eq "ConsoleHost")
{
Write-Host "Press any key to continue..."
$Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyUp") > $null
}
In the beginning, I did not have the -Path $PSScriptRoot and I ran my scripts using "Run with Powershell" in the context menu. This way the scripts seemed to use the scripts directory as the working directory. When I changed the default app to PowerShell, it started using PowerShell's install directory as the working directory. So I added -Path $PSScriptRoot and now the first script does not work; it opens a console and closes it right away. The second script works just fine.
Also, The first script still runs correctly if I "Run with Powershell" using the context menu.
What Am I doing wrong in the first script?
I am guessing that the path to your script has spaces in it. The problem has to do with the way windows passes the file argument to the application and the way the path to the script is being parsed.
When you run your script with "Run with Powershell", the following command is sent to the shell:
"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" "-Command" "if((Get-ExecutionPolicy ) -ne 'AllSigned') { Set-ExecutionPolicy -Scope Process Bypass }; & 'C:\Temp\with spaces\scriptRoot.ps1'"
You'll notice that the command is wrapped in quotes. Which means that the shell will pass the quoted part as an argument to the powershell executable and effectively strips the outer " quotes. So powershell sees this as a command:
if((Get-ExecutionPolicy ) -ne 'AllSigned') { Set-ExecutionPolicy -Scope Process Bypass }; & 'C:\Temp\with spaces\scriptRoot.ps1'
Now you'll notice that the path to the script is still wrapped in single quotes and everything still works even though the path has spaces.
When you run the script as "Open with ..." powershell, windows (the explorer shell) will just pass the path to the script wrapped in quotes. It looks like this:
"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" "C:\Temp\with spaces\scriptRoot.ps1"
If I open an command prompt and try to run the above command, I get the following:
#>"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" "C:\Temp\with spaces\scriptRoot.ps1"
C:\Temp\with : The term 'C:\Temp\with' is not recognized as the name of a cmdlet, function, script file, or operable
program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
At line:1 char:1
+ C:\Temp\with spaces\scriptRoot.ps1
+ ~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (C:\Temp\with:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
That's because powershell itself never sees the outer quotes. If I add an extra layer of '' it works:
#>"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" "'C:\Temp\with spaces\scriptRoot.ps1'"
C:\Temp\with spaces\scriptRoot.ps1
Unfortunately, I'm not sure how to fix this other than getting rid of the spaces in your path.

Different results when testing if remote UNC path exists in a for/foreach loop

When attempting to loop through a collection of ~3k machines to check if a folder path exists, both Test-Path (PowerShell) and 'IF EXIST' (Batch) return bogus results, saying that a remote path does not exist when it does, in fact, exist.
I'm running the PowerShell session and ISE (and Command Prompt) 'As An Administrator' with different authoritative domain creds than my user login account. I've supplied the -Credential parameter to Test-Path with no change in the results.
I'm running Win10 v1709 (10.0.16299.547) and PowerShell v5.1.16299.547.
When running the commands by themselves on a one-off machine name, they work:
Powershell:
Test-Path "\\machineName\c$\Program Files (x86)\Common Files\Folder Name"
Batch:
IF EXIST "\\machineName\c$\Program Files (x86)\Common Files\Folder Name" (echo True)
Both of the above examples return 'True' as expected.
However, when using these commands in for/foreach loops, I only get 'False' results :(
PowerShell:
$Computers = Get-Content c:\logs\computers.txt
Write-Output "Checking $($Computers.count) Machines ..."
foreach ($Computer in $Computers)
{
if (Test-Path "\\$Computer\c$\Program Files (x86)\Common Files\Folder Name")
{
Write-Output "$($Computer): Folder Exists"
}
}
Batch:
#echo off
for /f %%i in (C:\logs\computers.txt) do (
echo | SET /P nonewline=Checking %%i ...
IF EXIST "\\%%i\c$\Program Files (x86)\Common Files\Folder Name" (
echo Found
echo %%i >> c:\logs\folder_exists.txt
) ELSE (echo .)
)
pause
Both of these examples return nothing.
Where do I start looking for what could be causing this undesirable behavior?
Are there GPOs that might cause this result, that are applied from my domain?
In case the loop doesn't work, add some debugging logic. At the moment the script fails silently in two cases: 1) $Computers is empty 2) test-path has unexpected problems.
When one works on console, it's a common mistake to initialize some variables and never assign those in the scripted version. To catch such, add set-strictmode to the script so that it will complain when an uninitialized variable is used.
set-strictmode -Version 2.0
# Prohibits references to uninitialized variables
# and references to non-existent properties of an object.
# This eliminates common typos
if($Computers.Length -le 0) { # Is there data to process?
write-host '$Computers collection was empty!'
}
if($Computers.GetType().name -ne "Object[]") { # Is the collection an array anyway?
write-host '$Computers wasnt an array but: ' $Computers.GetType()
}
foreach ($Computer in $Computers)
{
if (Test-Path "\\$Computer\c$\Program Files (x86)\Common Files\Folder Name")
{
Write-Output "$($Computer): Folder Exists"
} else { # test-path fails. Write a warning and the path tried
write-host -nonewline "Fail: "
write-host "test-path \\$Computer\c$\Program Files (x86)\Common Files\Folder Name"
}
}
Batch resolution:
Opened the text file in Notepad++, and somehow the file's encoding got set to 'UCS-2 LE BOM' instead of the expected 'UTF-8'. Setting the encoding to UTF-8 and saving the file resolves the batch file issue.
I hit the same issue many years ago, so I'm a bit embarrassed I didn't think of that before posting.
PowerShell resolution:
I've recently starting using the SCCM module, and hadn't noticed that my script was setting my location to the "PS $SCCMsiteCode:>" PSDrive.
Simply typing 'c:' in the ISE console pane and hitting enter, allowed the script to return the expected results.
I didn't think the current location would prevent Write-Output from writing to the console pane.

Run a jar file from powershell

I want to run a jar file from powershell. Till now I have been able to find this:
Start-Process -FilePath java -ArgumentList '-jar Upload_Download.jar FilePickupPath= $pickuppath FileDownloadPath= $download' -PassThru -RedirectStandardError E:\stderr.txt
Some how this is not working. Any suggestions?
Powershell has multiple string quotation characters that behave different ways. The double quote " allows evaluations within the string whilst the single quote ' doesn't.
As a practical example:
$foo=42
write-host "The foo: $foo"
# Prints The foo: 42
write-host 'The foo: $foo'
# Prints The foo: $foo
The command uses single quote like so, (backtick is used to split the code into screen friendly format)
Start-Process -FilePath java `
-ArgumentList '-jar Upload_Download.jar FilePickupPath= $pickuppath fileDownloadPath= $download' `
-PassThru -RedirectStandardError E:\stderr.txt
This will pass literally $pickuppath and $download. The intention is likely to pass $pickuppath and $download's values.
In order to resolve the issue, use double quotes.
Passing the arguments is often a problem so I use an array,
also avoid long lines by using parameter splatting.
if(-not $env:JAVA_HOME)
{
Write-Error "JAVA_HOME not set"
break
}
$params = #{
FilePath = [string]::Format("{0}\bin\java.exe",$env:JAVA_HOME)
WorkingDirectory = "D:\SDL\Web\live\discovery\config\"
ArgumentList = #("-jar", "discovery-registration.jar", "update")
RedirectStandardError = "c:\temp\JavaError.txt"
PassThru = $true
Wait = $true
}
$p = Start-Process #params
if($p.ExitCode -eq 0)
{
Write-Output "Discovery Registration complete"
}
else
{
Write-Output "Discovery Registration failed"
}
My problem was a little different because there are two jre versions and one jdk installed and powershell was always picking up jre based java executable and failing with the message
C:\Program Files\Java\jre1.8.0_73 is not a valid JDK Java Home.
Although jdk was installed and JAVA_HOME variable was set and out of variable displays the jdk path correctly.
> $env:JAVA_HOME
C:\Program Files\Java\jdk1.8.0_73
I stored the jdk java executable path in a variable, navigated to the directory containing jar file for Web logic server and invoked the installation program using following code.
$java="C:\Program Files\Java\jdk1.8.0_73\bin\java"
Start-Process -FilePath $java -ArgumentList '-jar fmw_12.2.1.3.0_infrastructure_generic.jar'
The installation program opened after few seconds. I have omitted -RedirectStandardErro switch to keep the command small and simple although i did use it to catch any error in the first place.
I tried using variable $fmw to store the jar file fmw_12.2.1.3.0_infrastructure_generic.jar but it did not work. Unsuccessful code is as follows
$fmw="fmw_12.2.1.3.0_infrastructure_generic.jar"
Start-Process -FilePath $java -ArgumentList '-jar $fmw'
I hope this would help someone and somebody might provide me a better and clean method to call jar files in powershell.
When you run the command after defining the $java variable, make sure to put the full path of the jar in double quotes inside the single quotes. This is to fix the error that occurs when the path of the file has a space in it.
The other option is to cd into the folder with the jar and run the command that way, as suggested in the above answer. This also works and is helpful with simple paths.
Also when defining the java variable, try putting the path to the executable instead of just the folder. This prevents execution conflicts if paths are similar.

how to Execute a cmd file in my ps script?

I have a cmd file which calls an msi and passes paramters. I am calling this deploy.cmd file from a powershell script. How can i achive this?
I am may be missing something here.
This is what my cmd looks like,
Msiexec /i ABCInstaller.msi ^
DB.SERVER=ABC\QA ^
APPLICATION.ENV.TYPE=Qa ^
SVCIDENTITY=SVC-QA#ABC.com ^
SVCPASSWORD=xxx ^
LOCAL.EMAILING="true" ^
EMAIL.GMAT="tarun.arora#abc.com" ^
EMAIL.GMATR="tarun.arora#abc.com" ^
EMAIL.SUCCESSFUL.VALIDATION.SUBJECT="[QA] Successful validation of ABC Message" ^
/lv "ABC_Installer_QA_Log.txt"
This is what my powershell script looks like,
# Assigning Build Number and Drop Location for the MSI in scope
$buildNumber = $TfsDeployerBuildData.BuildNumber
$dropLocation = $TfsDeployerBuildData.DropLocation
# Assign values
if($buildNumber -eq $null)
{
$buildNumber = $args[0]
$dropLocation = $args[1]
}
# Move old uninstall folder to Archive folder
Move-Item "D:\deploy\ABC_Uninstalled\*" "D:\deploy\ABC_Archive" -force
# Move old build folder to uninstalled folder
Move-Item "D:\deploy\ABC_Installed\*" "D:\deploy\ABC_Uninstalled" -force
# Logging
Add-Content -Path "C:\Log\TfsDeployer_Log.txt" -Value $dropLocation
Add-Content -Path "C:\Log\TfsDeployer_Log.txt" -Value $buildNumber
# Copy the msi from drop location to local physical drive
Copy-Item $dropLocation "D:\deploy\ABC_Installed" -recurse
Add-Content -Path "C:\Log\TfsDeployer_Log.txt" -Value "Copied the Msi to D:\deploy\Installed"
# Start execution
& "D:\deploy\ABC_Installed\$buildNumber\en-us\ETRM_QA.cmd"
However when the ps is executed, It prints out what is inside the cmd file rather than executing it, so the output of the execution is
Output: C:\WINDOWS\system32>Msiexec /i ABCInstaller.msi ^
DB.SERVER=ABC\QA ^
APPLICATION.ENV.TYPE=Qa ^
SVCIDENTITY=SVC-QA#ABC.com ^
SVCPASSWORD=xxx ^
LOCAL.EMAILING="true" ^
EMAIL.GMAT="tarun.arora#abc.com" ^
EMAIL.GMATR="tarun.arora#abc.com" ^
EMAIL.SUCCESSFUL.VALIDATION.SUBJECT="[QA] Successful validation of ABC Message" ^
/lv "ABC_Installer_QA_Log.txt" /passive T
The cmd file is not executed :-(
Try:
Invoke-Expression "D:\deploy\ABC_Installed\$buildNumber\en-us\ETRM_QA.cmd"
MsiExec likely is executing, you are just not seeing it because it launches as a background process that immediately returns control to cmd. For example, if I create a cmd script that looks like this:
"C:\Program Files\Microsoft Office\Office11\WINWORD.EXE"
And invoke it like this:
&launchWord.cmd
All I see on the powershell console is the contents of the cmd script, but word opens in another window.
Are you sure msiexec isn't just launching and failing, rather than failing to launch?

How to reload user profile from script file in PowerShell

I want to reload my user profile from a script file. I thought that dot sourcing it from within the script file would do the trick, but it doesn't work:
# file.ps1
. $PROFILE
However, it does work if I dot source it from PowerShell's interpreter.
Why do I want to do this?
I run this script every time I update my profile and want to test it, so I'd like to avoid having to restart PowerShell to refresh the environment.
If you want to globally refresh your profile from a script, you will have to run that script "dot-sourced".
When you run your script, all the profile script runs in a "script" scope and will not modify your "global" scope.
In order for a script to modify your global scope, it needs to be "dot-source" or preceded with a period.
. ./yourrestartscript.ps1
where you have your profile script "dot-sourced" inside of "yourrestartscript.ps1". What you are actually doing is telling "yourrestartscript" to run in the current scope and inside that script, you are telling the $profile script to run in the script's scope. Since the script's scope is the global scope, any variables set or commands in your profile will happen in the global scope.
That doesn't buy you much advantage over running
. $profile
So, the approach that you marked as the answer may work inside the Powershell command prompt, but it doesn't work inside PowerShell ISE (which, to me, provides a superior PowerShell session) and probably won't work right in other PowerShell environments.
Here's a script that I have been using for a while, and it has worked very well for me in every environment. I simply put this function into my Profile.ps1 at ~\Documents\WindowsPowerShell, and whenever I want to reload my profile, I dot-source the function, i.e.
. Reload-Profile
Here's the function:
function Reload-Profile {
#(
$Profile.AllUsersAllHosts,
$Profile.AllUsersCurrentHost,
$Profile.CurrentUserAllHosts,
$Profile.CurrentUserCurrentHost
) | % {
if(Test-Path $_){
Write-Verbose "Running $_"
. $_
}
}
}
& $profile
works to reload the profile.
If your profile sets aliases or executes imports which fail then you will see errors because they were already set in the previous loading of the profile.
Why are you trying to do this?
Because it is likely to create duplicates (appends to $env:path) and problems with setting constant/readonly objects causing errors.
There was a thread on this topic recently on microsoft.public.windows.powershell.
If you are trying to reset the state of the session there is no way to do this, even using an inner scope ($host.EnterNestedPrompt()) because of the ability to set variables/aliases/... at "all scope".
I found this workaround:
#some-script.ps1
#restart profile (open new powershell session)
cmd.exe /c start powershell.exe -c { Set-Location $PWD } -NoExit
Stop-Process -Id $PID
A more elaborated version:
#publish.ps1
# Copy profile files to PowerShell user profile folder and restart PowerShell
# to reflect changes. Try to start from .lnk in the Start Menu or
# fallback to cmd.exe.
# We try the .lnk first because it can have environmental data attached
# to it like fonts, colors, etc.
[System.Reflection.Assembly]::LoadWithPartialName("System.Diagnostics")
$dest = Split-Path $PROFILE -Parent
Copy-Item "*.ps1" $dest -Confirm -Exclude "publish.ps1"
# 1) Get .lnk to PowerShell
# Locale's Start Menu name?...
$SM = [System.Environment+SpecialFolder]::StartMenu
$CurrentUserStartMenuPath = $([System.Environment]::GetFolderPath($SM))
$StartMenuName = Split-Path $CurrentUserStartMenuPath -Leaf
# Common Start Menu path?...
$CAD = [System.Environment+SpecialFolder]::CommonApplicationData
$allUsersPath = Split-Path $([System.Environment]::GetFolderPath($CAD)) -Parent
$AllUsersStartMenuPath = Join-Path $allUsersPath $StartMenuName
$PSLnkPath = #(Get-ChildItem $AllUsersStartMenuPath, $CurrentUserStartMenuPath `
-Recurse -Include "Windows PowerShell.lnk")
# 2) Restart...
# Is PowerShell available in PATH?
if ( Get-Command "powershell.exe" -ErrorAction SilentlyContinue ) {
if ($PSLnkPath) {
$pi = New-Object "System.Diagnostics.ProcessStartInfo"
$pi.FileName = $PSLnkPath[0]
$pi.UseShellExecute = $true
# See "powershell -help" for info on -Command
$pi.Arguments = "-NoExit -Command Set-Location $PWD"
[System.Diagnostics.Process]::Start($pi)
}
else {
# See "powershell -help" for info on -Command
cmd.exe /c start powershell.exe -Command { Set-Location $PWD } -NoExit
}
}
else {
Write-Host -ForegroundColor RED "Powershell not available in PATH."
}
# Let's clean up after ourselves...
Stop-Process -Id $PID
This is only a refinement of the two line script in guillermooo's answer above, which did not get the new PowerShell window into the correct directory for me. I believe this is because $PWD is evaluated in the new PowerShell window's context, which is not the value we want set-location to process.
function Restart-Ps {
$cline = "`"/c start powershell.exe -noexit -c `"Set-Location '{0}'" -f $PWD.path
cmd $cline
Stop-Process -Id $PID
}
By rights it shouldn't work, as the command line it spits out is malformed, but it seems to do the job and that's good enough for me.
since I stumbled onto this several years later, I thought to add that you can use the invocation operator: & to load your profile with the default variable to your profile: $profile.
so, if your session somehow fails to load your profile (happens to me with cmder/conemu) just type:
& $profile
I used this to troubleshoot what profile was taking forever to load.
Start Run:
powershell_ise -noprofile
Then i ran this:
function Reload-Profile {
#(
$Profile.AllUsersAllHosts,
$Profile.AllUsersCurrentHost,
$Profile.CurrentUserAllHosts,
$Profile.CurrentUserCurrentHost
) | % {
if(Test-Path $_){
Write-Verbose "Running $_"
$measure = Measure-Command {. $_}
"$($measure.TotalSeconds) for $_"
}
}
}
. Reload-Profile
Thank you #Winston Fassett for getting me closer to finding my issue.
Pseudo Alias (simulate keys)
If you just want a function to work like an alias in the console, just simulate the key presses to get around having to use the dot source.
# when "reload" is typed in the terminal, the profile is reloaded
# use sendkeys to send the enter key to the terminal
function reload {
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.SendKeys]::SendWait(". $")
[System.Windows.Forms.SendKeys]::SendWait("PROFILE")
[System.Windows.Forms.SendKeys]::SendWait("{ENTER}")
}
screenshot of it working