How to properly quote a PowerShell command path when used from CMD - powershell

Baseline command that works properly:
The following command is being executed in CMD prompt and runs successfully.
"C:\Program Files\PowerShell\6.0.4\pwsh.exe" -NoLogo -NoProfile -NonInteractive -Command "&{D:\FooBar\myscript.ps1 -DependentAssembliesDirectoryPath 'D:\FooBar' -OutputPath 'D:\Baz Qux\output' -DocumentVersion 'whatever' -VisualStudioXmlDocumentationPaths 'D:\Baz Qux\input\my.xml' -AssemblyPaths 'D:\Baz Qux\input\my.exe','D:\Baz Qux\input\my1.dll','D:\Baz Qux\input\my2.dll','D:\Baz Qux\input\my3.dll' -MajorOpenApiSpecificationVersion 3 -MinorOpenApiSpecificationVersion 0 -Format YAML -DocumentDescriptionFilePath 'D:\Baz Qux\input\my.md'}; EXIT $LASTEXITCODE"
However, when a space is introduced to the path of myscript.ps1, the command no longer works. This is expected since I need to properly quote the path. I cannot figure out the proper way of quoting though.
Invalid attempt at quoting the command:
I thought this would have worked based on the technique of quoting other paths in my command, but this doesn't work.
"C:\Program Files\PowerShell\6.0.4\pwsh.exe" -NoLogo -NoProfile -NonInteractive -Command "&{'D:\Foo Bar\myscript.ps1' -DependentAssembliesDirectoryPath 'D:\Foo Bar' -OutputPath 'D:\Baz Qux\output' -DocumentVersion 'whatever' -VisualStudioXmlDocumentationPaths 'D:\Baz Qux\input\my.xml' -AssemblyPaths 'D:\Baz Qux\input\my.exe','D:\Baz Qux\input\my1.dll','D:\Baz Qux\input\my2.dll','D:\Baz Qux\input\my3.dll' -MajorOpenApiSpecificationVersion 3 -MinorOpenApiSpecificationVersion 0 -Format YAML -DocumentDescriptionFilePath 'D:\Baz Qux\input\my.md'}; EXIT $LASTEXITCODE"
This command results in a bunch of errors like,
At line:1 char:119
+ ... ionsDocumentGeneration.ps1' -DependentAssembliesDirectoryPath 'D:\Ope ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Unexpected token '-DependentAssembliesDirectoryPath' in expression or statement.
At line:1 char:153
+ ... rectoryPath 'D:\Foo Bar' -OutputPath ...
+ ~~~~~~~~~~~~
Unexpected token ''D:\Foo Bar'' in expression or statement.
Again, since it can be a little hard to see the difference, the delta from the baseline command that works and the second command is that &{D:\FooBar\myscript.ps1 changed to &{'D:\Foo Bar\myscript.ps1' to introduce a space in the path and attempt quoting.
Please Note
I cannot invoke the command in PowerShell because it is out of my control. It must be invoked in cmd.exe prompt.

The problem is that you need to use & in PowerShell in order to invoke a command name / executable file path that is quoted and/or specified via a variable:
Therefore, replace:
"&{'D:\Foo Bar\myscript.ps1' ... }; exit $LASTEXITCODE"
with:
"& { & 'D:\Foo Bar\myscript.ps1' ... }; exit $LASTEXITCODE"
That said, there's no reason to wrap the invocation of your *.ps1 script in a script-block invocation (& { ... }), so you can simplify your command to:
"& 'D:\Foo Bar\myscript.ps1' ...; exit $LASTEXITCODE"

Related

Returning an exit code from a PowerShell script

I have questions about returning an exit code value from PowerShell when run from a cmd.exe invocation. I found https://weblogs.asp.net/soever/returning-an-exit-code-from-a-powershell-script which has been helpful. But, the solution for PowerShell code is to add a function.
function ExitWithCode { param($exitcode) $host.SetShouldExit($exitcode) exit }
Generates:
ExitWithCode : The term 'ExitWithCode' 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 C:\src\t\e\exit5.ps1:6 char:1
+ ExitWithCode -exitcode 12345
+ ~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (ExitWithCode:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
But, placing the "exit" on a new line works. Is this just a language anomaly?
function ExitWithCode { param($exitcode) $host.SetShouldExit($exitcode)
exit }
Also, this page is from 2010. Is this still the current state? Is there a better/easier way now?
As Guenther Schmitz already explained, $host.SetShouldExit($exitcode) and exit are 2 distinct statements that must be separated either with a newline or a semicolon.
Without that separation your code should have thrown a different error, though:
Unexpected token 'exit' in expression or statement.
The error you posted looks more like you tried to use that function without defining it first.
The purpose of the function is to set a proper exit code when exiting from a script regardless of how the script was run. Normally you'd run PowerShell scripts like this:
powershell.exe -File "C:\your.ps1"
And in that case a simple exit $exitcode would be sufficient:
C:\> type test1.ps1
exit 23
C:\> powershell -File .\test1.ps1
C:\> echo %errorlevel%
23
However, another way to execute PowerShell scripts is the -Command parameter (since PowerShell scripts can be run directly from PowerShell). The difference between the -File and -Command parameters is that the latter returns only 1 or 0 (indicating whether or not the script exited with a non-zero exit code), but not the exit code itself.
C:\> powershell -Command .\test1.ps1
C:\> echo %errorlevel%
1
When omitting the parameter entirely PowerShell defaults to -Command (allowing you to easily run PowerShell statements from the commandline):
C:\> powershell .\test1.ps1
C:\> echo %errorlevel%
1
Defining an exit code via $host.SetShouldExit() ensures that the exit code is returned correctly when the script is invoked via powershell. -Command. You still should exit with the actual exit code, though, because otherwise the exit code would only be set when running the script via powershell.exe -Command, but not when running the script via powershell.exe -File:
C:\> type test2.ps1
function ExitWithCode($exitcode) {
$host.SetShouldExit($exitcode)
exit
}
ExitWithCode 23
C:\> powershell -File .\test2.ps1
C:\> echo %errorlevel%
0 # ← exit without argument defaults to 0!
C:\> powershell -Command .\test2.ps1
C:\> echo %errorlevel%
23
C:\> type test3.ps1
function ExitWithCode($exitcode) {
$host.SetShouldExit($exitcode)
exit $exitcode
}
ExitWithCode 23
C:\> powershell -File .\test3.ps1
C:\> echo %errorlevel%
23
C:\> powershell -Command .\test3.ps1
C:\> echo %errorlevel%
23
Guenther Schmitz's answer solves your immediate syntax problem, but it's important to note
that $host.SetShouldExit() is not meant to be called by user code, as implied by Bruce Payette's answer.
Instead, it is used internally by PowerShell itself in response to an exit statement in user code.
The only conceivable reason to use it is to repurpose it for implementing a workaround around a limitation of exit-code reporting when a script is being called via the -Command
(-c) parameter of PowerShell's CLI:
With -Command, a script's specific non-zero exit code is always translated to 1, so the specific exit code is lost - see this answer for an overview of exit-code handling in PowerShell.
$host.SetShouldExit(), despite not being intended for this purpose, happens to overcome this limitation and ensures that the script's exit code is also reported as the PowerShell process' exit code.
This workaround must not be applied when the script is being called from an interactive PowerShell session, because it will cause the session as a whole to exit instantly, which is the subject of your follow-up question.
Reliable detection of when a script is being called via -Command is nontrivial and hard to make fully reliable, however, as shown in this answer to your follow-up question.
The better approach is not to use the $host.SetShouldExit() workaround and do the following instead.
Invoke your script via the -File CLI parameter, in which case no workaround is needed:
Just use exit $n in your script, and $n will be reported as the PowerShell process' exit code (assuming $n is an integer).
When calling via -Command, follow the script call with ; exit $LASTEXITCODE in the command string so as to ensure that the script's exit code is passed through.
Of course, you may not always be in control of how your script is invoked via the CLI; in that event, the workaround is worth considering.
$host.SetShouldExit($exitcode) and exit are two commands which have to separated. either with a return (like you mentioned) or with a semicolon:
function ExitWithCode { param($exitcode) $host.SetShouldExit($exitcode); exit }
The way to return an exit code from PowerShell is to do exit $exitcode. Internally, When the runtime processes the exit keyword it will do the call to $host.SetShouldExit($exitcode) for you. But be aware that exit also exits scripts so it matters how you run your script. If want to run a script that calls exit from powershell.exe use the -File parameter rather than the -Script as in
powershell -File scriptThatCallsExit

How can I run a PowerShell script with white spaces in the path from the command line?

So I've tried a bunch of different ways to run a PowerShell script from the command line and every single one returns an error.
Here is this path:
C:\Users\test\Documents\test\line space\PS Script\test.ps1
I've tried these:
powershell -File '"C:\Users\test\Documents\test\line space\PS Script\test.ps1"'
powershell "& ""C:\Users\test\Documents\test\line space\PS Script\test.ps1"""
Powershell "& 'C:\Users\test\Documents\test\line space\PS Script\test.ps1'"
Powershell -File 'C:\Users\test\Documents\test\line space\PS Script\test.ps1'"
I get all these errors:
& : The term 'C:\Users\test\Documents\test\line space\PS Script' 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.
Processing -File ''C:\Users\test\Documents\test\line space\PS Script'' failed: The given path's format is not support ed. Specify a valid path for the -File parameter.
How can I fix this?
The -File parameter
If you want to run powershell.exe -File from the command line, you always have to set paths with spaces in double quotes ("). Single quotes (') are only recognized by PowerShell. But as powershell.exe is invoked (and hence the file parameter processed) by the command line, you have to use ".
powershell.exe -File "C:\Users\test\Documents\Test Space\test.ps1" -ExecutionPolicy Bypass
The -Command parameter
If you use the -Command parameter, instead of -File, the -Command content is processed by PowerShell. Hence you can - and in this case have to - use ' inside ".
powershell.exe -Command "& 'C:\Users\test\Documents\Test Space\test.ps1'" -ExecutionPolicy Bypass
The double quotes are processed by the command line, and & 'C:\Users\test\Documents\Test Space\test.ps1' is a command that is actually processed by PowerShell.
Solution 1 is obviously simpler.
Note that -Command is also the default parameter that is used, if you do not specify any.
powershell.exe "& 'C:\Users\test\Documents\Test Space\test.ps1'" -ExecutionPolicy Bypass
This would work, too.
The -EncodedCommand parameter
You can encode your command as Base64. This solves many "quoting" issues and is sometimes (but not in your case though) the only possible way.
First you have to create the encoded command
$Command = "& 'C:\Users\test\Documents\Test Space\test.ps1'"
[Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($Command))
And then you can use the the -EncodedCommand parameter like this
powershell.exe -EncodedCommand JgAgACcAQwA6AFwAVQBzAGUAcgBzAFwAdABlAHMAdABcAEQAbwBjAHUAbQBlAG4AdABzAFwAVABlAHMAdAAgAFMAcABhAGMAZQBcAHQAZQBzAHQALgBwAHMAMQAnAA== -ExecutionPolicy Bypass
Try this:
& "C:\Users\test\Documents\test\line space\PS Script\test"
In your examples, you're mixing quotes and double quoting for no reason.
IF EXIST "C:\Users\test\Documents\test\line space\PS Script\test.ps1" (
powershell -ExecutionPolicy Unrestricted -File "C:\Users\test\Documents\test\line space\PS Script\test.ps1"
)
In case you use parameters you can do as follows.
powershell.exe -command "& {&'C:\A B C\foo.ps1' param1 param2}"
Thanks at this point to a blog post by Hesham A. Amin :-)
I needed to pass a parameter with spaces.
I am dragging and dropping a file onto a batch file, and the file is off on the server with spaces in the path and/or file name. After testing the above answers, I got this to work. Note I am changing to the working directory prior to starting the PowerShell executable.
Batch file:
pushd O:\Data\QuickList
start powershell -noexit -Command ".\QuickList.ps1 -datafile '%1'"
popd

Capturing different streams in file

I'm trying to capture the Verbose, Error and other streams of a PowerShell script in a file. This to monitor the output of my script.
The following code works fine:
$LogFile = 'S:\ScriptLog.log'
$ScriptFile = 'S:\TestieScript.ps1'
powershell -Command $ScriptFile *>&1 > $LogFile
However, the moment I try to put a space in one of the file paths, it's no longer working. I tried a lot of things, like double quotes, single quotes, .. but no luck.
To illustrate, the following code doesn't work:
$LogFile = 'S:\ScriptLog.log'
$ScriptFile = 'S:\Testie Script.ps1'
powershell -Command $ScriptFile *>&1 > $LogFile
One person in this thread has the same issue.
Thank you for your help.
You're trying to run a file whose name contains a space as a command without proper quoting, so you're most likely getting an error like this in your log:
The term 'S:\Testie' is not recognized as the name of a cmdlet, function, script file, or operable program.
Either add proper quoting (and the call operator &, because your path is now a string):
powershell -Command "& '$ScriptFile'" *>&1 > $LogFile
or (better) use the -File parameter, as #CB. already suggested:
powershell -File $ScriptFile *>&1 > $LogFile
which has the additional advantage that the call will return the actual exit code of the script.
Edit: If you want to run the command as a scheduled task you'll need to use something like this:
powershell -Command "& 'S:\Testie Script.ps1' *>&1 > 'S:\ScriptLog.log'; exit $LASTEXITCODE"
because the redirection operators only work inside a PowerShell process.
try using -file parameter:
powershell -file $ScriptFile *>&1 > $LogFile

passing \ in argument to powershell script causes unexpected escaping

This is my powershell script test.ps1:
Write-Output $args;
Now suppose I have a batch script that calls this powershell script with all kinds of paths. One of those is c:\:
powershell -executionpolicy Bypass -file test.ps1 "c:\"
The output is:
c:"
Is there any way to quote my arguments such that c:\ would actually be taken and stored as is in the $args[0] variable? I know I can solve this quick'dirty by passing "c:\\", but that's not a real solution.
EDIT: using named parameters in test.ps1 doesn't make any difference:
[CmdletBinding()]
param(
[string]$argument
)
Write-Output $argument;
EDIT2: using a batch file instead works fine.
My test.bat script:
echo %~1
I run it:
test.bat "c:\"
Returns nicely:
c:\
Are you sure this comes form powershell and not from the program which invokes your statement? The backslash is no escape code in powershell.
my test.ps1 is working, when run from ise.
this works for me:
powershell -executionpolicy Bypass -command "test.ps1 -argument 'C:\'"
(end with quote double-quote)
Help file for PowerShell.exe says:
File must be the last parameter in the command, because 'all characters' typed after the file parameter name are "interpreted" as the script file path followed by the script parameters.
You are against Powershell.exe's command line parser, which uses "\" to escape quotes. Do you need quotes? Not in your case:
powershell -file test.ps1 c:\
prints
c:\
Similarly, this works too
powershell -file test.ps1 "c:\ "
c:\
but then your arg has that extra space which you would want to trim. BTW, Single quotes do not help here:
powershell -file test.ps1 'c:\'
'c:\'
If you need the final backlash to be passed to the command, you can use
$ArgWithABackslashTemp = $ArgWithABackslash -replace '\\$','\\'
&$ExePath $ArgWithABackslashTemp
Or, if the exe is smart enough to handle it without the trailing backslash
&$ExePath $ArgWithABackslash.trim('\')

How to run a Powershell script from the command line and pass a directory as a parameter

PowerShell -Command .\Foo.ps1
Foo.ps1:
Function Foo($directory)
{
echo $directory
}
if ($args.Length -eq 0)
{
echo "Usage: Foo <directory>"
}
else
{
Foo($args[0])
}
Despite Foo.ps1 being in the directory from where I am calling Powershell, this results in:
The term '.\Foo.ps1' 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.
EDIT: Wasn't working because PowerShell was changing directory due to profile.ps1 containing cd C:\
I then tried to call it specifying the full path to the script file, but no matter what I try, I can't get it to work. I believe I have to quote the path because it contains whitespaces, as does the file name I need to pass in an argument to the script.
Best guess so far:
PowerShell -Command "'C:\Dummy Directory 1\Foo.ps1' 'C:\Dummy Directory 2\File.txt'"
Outputs error:
Unexpected token 'C:\Dummy Directory 2\File.txt' in expression or statement.
At line:1 char:136.
try this:
powershell "C:\Dummy Directory 1\Foo.ps1 'C:\Dummy Directory 2\File.txt'"
you are calling a script file not a command so you have to use -file eg :
powershell -executionPolicy bypass -noexit -file "c:\temp\test.ps1" "c:\test with space"
for PS V2
powershell.exe -noexit &'c:\my scripts\test.ps1'
(check bottom of this technet page http://technet.microsoft.com/en-us/library/ee176949.aspx )
Using the flag -Command you can execute your entire powershell line as if it was a command in the PowerShell prompt:
powershell -Command "& '<PATH_TO_PS1_FILE>' '<ARG_1>' '<ARG_2>' ... '<ARG_N>'"
This solved my issue with running PowerShell commands in Visual Studio Post-Build and Pre-Build events.
Add the param declation at the top of ps1 file
test.ps1
param(
# Our preferred encoding
[parameter(Mandatory=$false)]
[ValidateSet("UTF8","Unicode","UTF7","ASCII","UTF32","BigEndianUnicode")]
[string]$Encoding = "UTF8"
)
write ("Encoding : {0}" -f $Encoding)
result
C:\temp> .\test.ps1 -Encoding ASCII
Encoding : ASCII
Change your code to the following :
Function Foo($directory)
{
echo $directory
}
if ($args.Length -eq 0)
{
echo "Usage: Foo <directory>"
}
else
{
Foo([string[]]$args)
}
And then invoke it as:
powershell -ExecutionPolicy RemoteSigned -File "c:\foo.ps1" "c:\Documents and Settings" "c:\test"
you have type and hit enter :
PowerShell -Command