In powershell spawn notepad++ when file to open has spaces in it - powershell

$npp = "C:\Program Files\Notepad++\notepad++.exe";
$myfiles = #(
"C:\bad boys\file1.txt",
"C:\bad boys\file2.txt",
"C:\bad boys\file3.txt"
)
foreach ($file in $myfiles) {
Start-Process -FilePath $npp -ArgumentList "$file" -PassThru -NoNewWindow | out-null
}
This almost works... except, It doesn't open in notepad++ because it sees the space in the file name and thinks this is where the file path ends... thus, i am unable to open my file list. Any Ideas how to fix? What i get instead is notepad++ asking many times if I want to create the file "C:\bad"

tl;dr
While Joel Coehoorn's helpful answer provides an effective solution to your Start-Process problem (which stems from the bug detailed below), you can simplify your code to:
foreach ($file in $myfiles) {
# Note: | Out-Null is a trick that makes calling *GUI* applications
# *synchronous* (makes PowerShell wait for them to exit).
& $npp $file | Out-Null
}
You're seeing a long-standing bug in Start-Process that causes it to blindly space-concatenate its -ArgumentList (-Args) arguments without using required embedded double-quoting for arguments with spaces when forming the single string encoding all arguments that is passed to the target executable behind the scenes.
See GitHub issue #5576, which also discusses that a fix will require a new parameter so as not to break backward compatibility.
For that reason, the required embedded double-quoting must be performed manually as shown in Joel's answer.
When passing multiple arguments, it is ultimately easier to pass a single string to -ArgumentList, with embedded double-quoting as necessary - essentially by formulating a string similar to how you would pass multiple arguments from cmd.exe:
E.g., if you were to pass two file paths with spaces to Notepad++ at once, you would do:
Start-Process -Wait -FilePath $npp -ArgumentList "`"C:\bad boys\file1.txt`" `"C:\bad boys\file2.txt`""
Alternatively, since your argument string doesn't require string interpolation, you could use a verbatim (single-quoted) string instead, which avoids the need for escaping the embedded " as `":
Start-Process -Wait -FilePath $npp -ArgumentList '"C:\bad boys\file1.txt`" `"C:\bad boys\file2.txt"'
Using a here-string is yet another option that avoids the need to escape, and can additionally make the call more readable (also works with single quotes (#'<newline>...<newline>'#):
Start-Process -Wait -FilePath $npp -ArgumentList #"
"C:\bad boys\file1.txt" "C:\bad boys\file2.txt"
"#
Also note the overall simplification of the Start-Process call:
Use of -Wait to ensure synchronous execution (waiting for Notepad++ to exit before continuing).
It looks like this is what you tried to do by combining -PassThru with piping to Out-Null, but that doesn't actually work, because that only waits for Start-Process itself to exit (which itself - unlike the launched process - executes synchronously anyway).
The omission of the unnecessary -NoNewWindow parameter, which only applies to starting console applications (in order to prevent opening a new console window); Notepad++ is a GUI application.
Note that the only good reason to use Start-Process here - rather than direct invocation - is the need for synchronous execution: Start-Process -Wait makes launching GUI applications synchronous (too), whereas with direct invocation only console applications execute synchronously.
If you didn't need to wait for Notepad++ to exit, direct invocation would make your quoting headaches would go away, as the required embedded quoting is then automatically performed behind the scenes:[1]
foreach ($file in $myfiles) {
& $npp $file # OK, even with values with spaces
}
However, the | Out-Null trick can be used effectively in direct invocation to make calling GUI applications synchronous[2], which leads us to the solution at the top:
foreach ($file in $myfiles) {
& $npp $file | Out-Null # Wait for Notepad++ to exit.
}
[1] However, up to at least PowerShell 7.2.x, other quoting headaches can still arise, namely with empty-string arguments and arguments whose values contain " chars. - see this answer.
[2] Out-Null automatically makes PowerShell wait for the process in the previous pipeline segment to exit, so as to ensure that all input can be processed - and it does so irrespective of whether the process is a console-subsystem or GUI-subsystem application. Since GUI applications are normally detached from the calling console and therefore produce no output there, Out-Null has no ill effects. In the rare event that a GUI application does explicitly attach to the calling console and produce output there, you can use | Write-Output instead (which also works if there's no output, but is perhaps more confusing).

Try quotes around the file paths within the string data:
$myfiles = #(
"`"C:\bad boys\file.txt`"",
"`"C:\bad boys\file2.txt`"",
"`"C:\bad boys\file3.txt`""
)

Related

How to pass an array to the arguments in Start-Process in Powershell?

I am writing a script to play certain files in a player. I use Get-ChildItem to get an array of file names. Then I want to use Start-Process to play these files. However, how can I add these file names to the arguments of the player program?
I used Start-Process -FilePath "C:\Program Files\DAUM\PotPlayer\PotPlayerMini64.exe" -ArgumentList $selected_items but it seems it doesn't work and the files are not played.
Notice there are spaces in the file names.
Syntax-wise, your approach should work, but doesn't, due to an unfortunate bug, still present in PowerShell 7.2 - see GitHub issue #5576.
While passing an array of arguments to Start-Process's -ArgumentList parameter does cause the array elements to be passed as individual arguments (which is usually how external CLIs expect multiple file arguments), the necessary double-quoting around elements with spaces is not applied when the command line ultimately used for invocation is constructed behind the scenes.
Also, for robustness you should use the .FullName property of the objects stored in $selected_items, so as to ensure that full paths are passed, because - in Windows PowerShell, situationally - Get-ChildItem's output objects may stringify to the file name only - see this answer.
Workaround: Pass a single argument to -ArgumentList, in which you encode all pass-through arguments, using embedded double-quoting.
Start-Process `
-FilePath "C:\Program Files\DAUM\PotPlayer\PotPlayerMini64.exe" `
-ArgumentList ($selected_items.ForEach({ '"{0}"' -f $_.FullName }) -join ' ')
Taking a step back:
If PotPlayerMini64.exe is a Windows GUI(-subsystem) application, you don't need Start-Process at all, because even direct invocation will then act asynchronously (i.e., the program will launch, and control will return to PowerShell right away; conversely, if you wanted to wait for the program to exit, use Start-Process -Wait).
& "C:\Program Files\DAUM\PotPlayer\PotPlayerMini64.exe" $selected_Items.FullName
Note that in direct invocations such as this, PowerShell does perform the necessary double-quoting behind the scenes, on demand.
Note: I'm unclear on whether passing multiple file paths to PotPlayerMini64.exe alone also starts playback - the alternative solution in the next section may ensure that.
Alternative, clipboard-based solution:
Judging by PotPlayerMini64.exe's available command-line options[1], the following may work (I cannot personally verify):
/clipboard :Appends content(s) from clipboard into playlist and starts playback immediately.
# Copy the full names of the files of interest to the clipboard.
Set-Clipboard -Value $selected_items.FullName
# Launch the player and tell it to start playback of the files on the clipboard.
# Parameters -FilePath and -ArgumentList are positionally implied.
Start-Process 'C:\Program Files\DAUM\PotPlayer\PotPlayerMini64.exe' /clipboard
There are file-arguments-related options such as /new, /insert, and /add, but it's unclear to me whether they - or omitting them altogether, as in your attempt - automatically start playback (may depend on the application's persistent configuration).
[1] Note that this is not the official documentation; I couldn't find the latter.
You can ForEach-Object:
Get-ChildItem . | ForEach-Object {Start-Process -FilePath "C:\Program Files\DAUM\PotPlayer\PotPlayerMini64.exe" -ArgumentList $_.FullName}
You don't need start-process (plus, -argumentlist doesn't handle filenames with spaces as easily).
& "C:\Program Files\DAUM\PotPlayer\PotPlayerMini64.exe"
C:\Program` Files\DAUM\PotPlayer\PotPlayerMini64.exe
$env:path += ';C:\Program Files\DAUM\PotPlayer'; PotPlayerMini64
Even using start-process, it varies with the program. For example, Emacs can take multiple file arguments, separated by spaces. If the filename has a space, it would need to be double quoted. External programs don't know what arrays are, and start-process converts them to one string with spaces in between each element.
start emacs file1,file2,'"my file"'
get-wmiobject win32_process | ? name -eq emacs.exe | % commandline
"c:\program files\emacs\bin\emacs.exe" file1 file2 "my file"
ps emacs | % commandline # ps 7
"c:\program files\emacs\bin\emacs.exe" file1 file2 "my file"

Start-Process, Invoke-Command or?

Using the program got your back or GYB. I run the following command
Start-Process -FilePath 'C:\Gyb\gyb.exe' -ArgumentList #("--email <Email Address>", "--action backup", "--local-folder $GYBfolder", "--service-account", "--batch-size 4") -Wait
The issue is that when the process is done my script does not complete.
$GYBfolder = $GYBfolder.Replace('"', "")
$output = [PSCustomObject]#{
Name = $SourceGYB
Folder = $GYBfolder
}
$filename = "C:\reports\" + $SourceGYB.Split("#")[0] + "_Backup.csv"
$output | Export-Csv $filename -NoTypeInformation | Format-Table text-align=left -AutoSize
Return $filename
For some reason the script stops right before the return.
I am curious to know if I should be using a different command to run GYB?
Any thoughts on why the script does not process the return?
There's great information in the comments, but let me attempt a systematic overview:
To synchronously execute external console applications and capture their output, call them directly (C:\Gyb\gyb.exe ... or & 'C:\Gyb\gyb.exe' ...), do not use Start-Process - see this answer.
Only if gyb.exe were a GUI application would you need **Start-Process -Wait in order to execute it synchronously**.
A simple, but non-obvious shortcut is to pipe the invocation to another command, such as Out-Null, which also forces PowerShell to wait (e.g. gyb.exe | Out-Null) - see below.
When Start-Process is appropriate, the most robust way to pass all arguments is as a single string encoding all arguments, with appropriate embedded "..." quoting, as needed; this is unfortunate, but required as a workaround for a long-standing bug: see this answer.
Invoke-Command's primary purpose is to invoke commands remotely; while it can be used locally, there's rarely a good reason to do so, as &, the call operator is both more concise and more efficient - see this answer.
When you use an array to pass arguments to an external application, each element must contain just one argument, where parameter names and their values are considered distinct arguments; e.g., you must use #(--'action', 'backup', ...) rather than
#('--action backup', ...)
Therefore, use the following to run your command synchronously:
If gyb.exe is a console application:
# Note: Enclosing #(...) is optional
$argList = '--email', $emailAddress, '--action', 'backup', '--local-folder', $GYBfolder, '--service-account', '--batch-size', 4
# Note: Stdout and stderr output will print to the current console, unless captured.
& 'C:\Gyb\gyb.exe' $argList
If gyb.exe is a GUI application, which necessitates use of Start-Process -Wait (a here-string is used, because it makes embedded quoting easier):
# Note: A GUI application typically has no stdout or stderr output, and
# Start-Process never returns the application's *output*, though
# you can ask to have a *process object* returned with -PassThru.
Start-Process -Wait 'C:\Gyb\gyb.exe' #"
--email $emailAddress --action backup --local-folder "$GYBfolder" --service-account --batch-size 4
#"
The shortcut mentioned above - piping to another command in order to force waiting for a GUI application to exit - despite being obscure, has two advantages:
Normal argument-passing syntax can be used.
The automatic $LASTEXITCODE variable is set to the external program's process exit code, which does not happen with Start-Process. While GUI applications rarely report meaningful exit codes, some do, notably msiexec.
# Pipe to | Out-Null to force waiting (argument list shortened).
# $LASTEXITCODE will reflect gyb.exe's exit code.
# Note: In the rare event that the target GUI application explicitly
# attaches to the caller's console and produces output there,
# pipe to `Write-Output` instead, and possibly apply 2>&1 to
# the application call so as to also capture std*err* output.
& 'C:\Gyb\gyb.exe' --email $emailAddress --action backup | Out-Null
Note: If the above unexpectedly does not run synchronously, the implication is that gyb.exe itself launches another, asynchronous operation. There is no generic solution for that, and an application-specific one would require you to know the internals of the application and would be nontrivial.
A note re argument passing with direct / &-based invocation:
Passing an array as-is to an external program essentially performs splatting implicitly, without the need to use #argList[1]. That is, it passes each array element as its own argument.
By contrast, if you were to pass $argList to a PowerShell command, it would be passed as a single, array-valued argument, so #argList would indeed be necessary in order to pass the elements as separate, positional arguments. However, the more typical form of splatting used with PowerShell commands is to use a hashtable, which allows named arguments to be passed (parameter name-value pairs; e.g., to pass a value to a PowerShell command's
-LiteralPath parameter:
$argHash = #{ LiteralPath = $somePath; ... }; Set-Content #argHash
[1] $args and #args are largely identical in this context, but, strangely, #argList, honors use of --%, the stop-parsing symbol operator, even though it only makes sense in a literally specified argument list.

Run Batch file with Arguments & Return Code in Powershell [closed]

Closed. This question is not reproducible or was caused by typos. It is not currently accepting answers.
This question was caused by a typo or a problem that can no longer be reproduced. While similar questions may be on-topic here, this one was resolved in a way less likely to help future readers.
Closed 1 year ago.
Improve this question
UPDATE: Solved, see comment
I'm trying to run a batch file inside a powershell script, where I need to pass two arguments and read the return code.
I tried and read $LASTEXITCODE, but it always returns me "0".
This doesn't pass the arguments to the batch file:
& $BATCH_PATH $REQUIRED_PARAMETERS $OPTIONAL_PARAMETERS | Out-File -FilePath "$COMPARE_DIR\$LOG_FILENAME.log" -Append
This works, but LASTEXITCODE is always "0":
cmd.exe /c "$($ROOT_DIR)\batch.bat $REQUIRED_PARAMETERS $OPTIONAL_PARAMETERS" | Out-File -FilePath "$COMPARE_DIR\$LOG_FILENAME.log" -Append
This works, but LASTEXITCODE is always "0":
Start-Process -FilePath "C:\Windows\System32\cmd.exe" -ArgumentList "/c", "$BATCH_PATH", "$REQUIRED_PARAMETERS", "$OPTIONAL_PARAMETERS" -WindowStyle Hidden -PassThru -Wait -RedirectStandardOutput "$COMPARE_DIR\$(Get-Random).log"
I also tried to get the ExitCode-Property of the Process-Object, but it is also 0.
tl;dr
Rather than having to modify your batch file by adding an exit /b %ERRORLEVEL% statement, you can modify the batch file's invocation by appending & exit in order to achieve the same effect:
cmd.exe /c "$($ROOT_DIR)\batch.bat $REQUIRED_PARAMETERS $OPTIONAL_PARAMETERS & exit" |
Out-File -FilePath "$COMPARE_DIR\$LOG_FILENAME.log" -Append
This obscure solution is necessitated by the unfortunate fact that cmd.exe doesn't reliably relay a batch file's exit code as its process exit code when the batch file is called from the outside, neither when a batch file is called directly nor via cmd.exe /c - see this answer for details.
Note:
The ie function that comes with the Native module (Install-Module Native) automatically applies this workaround, so that invocation as
ie "$($ROOT_DIR)\batch.bat" ... would work as-is with respect to setting $LASTEXITCODE (though you'd have to pass the arguments individually or via array variables, as discussed).
GitHub proposal #15143 advocates building this workaround into PowerShell itself, as part of a larger proposal to improve argument-passing to external programs on Windows, but the proposal was rejected.
As for what you tried:
This doesn't pass the arguments to the batch file:
& $BATCH_PATH $REQUIRED_PARAMETERS $OPTIONAL_PARAMETERS`
This syntax works in principle, but not if your variables contain multiple arguments as a single string. E.g., $REQUIRED_PARAMETERS = 'foo bar' would not work, but specifying the arguments a an an array of strings would, $REQUIRED_PARAMETERS = 'foo', 'bar': In the former case, 'foo bar' is passed as a single argument, in the latter case, the array elements become individual arguments.
This works, but LASTEXITCODE is always "0":
Start-Process -FilePath "C:\Windows\System32\cmd.exe" -ArgumentList "/c", "$BATCH_PATH", "$REQUIRED_PARAMETERS", "$OPTIONAL_PARAMETERS" -WindowStyle Hidden -PassThru -Wait -RedirectStandardOutput "$COMPARE_DIR\$(Get-Random).log"
I also tried to get the ExitCode-Property of the Process-Object, but it is also 0.
The automatic $LASTEXITCODE variable is only set for direct calls to external programs, not when you use Start-Process - which is generally the wrong tool for invoking console applications.
(As your statement implies, it is possible to get a process' exit code when Start-Process is used, namely by adding -Wait -PassThru to the call in order to wait for the process to exit and to return a System.Diagnostics.Process instance whose exit code can then be inspected.
In the case at hand this wouldn't make a difference anyway, because the problem lies with cmd.exe itself, not with how it is invoked.)

RoboCopy.exe switches not working in Powershell script

I'm going to try to make this question very simple. If I run the following, it works without issue:
$localData = "C:\"
$netData = "\\OtherPC\shared\"
Start-Process "RoboCopy.exe" -argumentlist " `"$localData`" `"$netData`" "
However, as soon as I add any switches (see below), it does not work. No errors to be seen.
$localData = "C:\"
$netData = "\\OtherPC\shared\"
Start-Process "RoboCopy.exe" -argumentlist " `"$localData`" `"$netData`" /copyall"
It's been a real pain to try and fix, any help is appreciated.
EDIT:
Fixed, and here is the final working syntax:
$localFiles = "C:\Users\"
$netFiles = "\\netLocation\migration\Users\"
Start-Process "RoboCopy.exe" -NoNewWindow -argumentlist " `"$localFiles\`" `"$netFiles\`" /s /r:1 /w:5 /mt:16"
As an aside: Your use of Start-Process may be intentional to run a lengthy operation asynchronously, in a new window, but the more typical case is to execute console applications such as Robocopy.exe synchronously, in the same window, in which case you need to call them directly (c:\path\to\some.exe ... or & $exePath ...) - see this answer. An alternative to asynchronous execution in a new window is to use a background job.
You have to double a trailing \ in your path arguments if they (end up) enclosed in "..." in the command line (ultimately) submitted:
Simply define your variable values with a trailing \\, which should make your command work:
$localData = 'C:\\'
$netData = '\\OtherPC\shared\\'
Another solution (on Windows) for file paths specifically is to insert a space between the trailing \ and the ". This works, because the file-system WinAPI calls ignore trailing spaces in paths.
The reason that \\ is needed at the end is that RoboCopy.exe (and, indeed, virtually all external programs) interpret sequence \" as an escaped " character rather to be used verbatim rather than having syntactic function.
If you need to append the extra \ programmatically, use the following:
$localDataEscaped = $localData -replace '\\$', '\\'
$netDataEscaped = $netData -replace '\\$', '\\'
Debugging tip for Start-Process calls:
(Temporarily) add -NoNewWindow -Wait to the call, which makes the command output appear in the same window, giving you a chance to inspect it; by default, Start-Process runs the command in a new window (on Windows) that automatically closes when the command finishes, potentially not giving you enough time to see the output.

Call a PowerShell script in a new, clean PowerShell instance (from within another script)

I have many scripts. After making changes, I like to run them all to see if I broke anything. I wrote a script to loop through each, running it on fresh data.
Inside my loop I'm currently running powershell.exe -command <path to script>. I don't know if that's the best way to do this, or if the two instances are totally separate from each other.
What's the preferred way to run a script in a clean instance of PowerShell? Or should I be saying "session"?
Using powershell.exe seems to be a good approach but with its pros and cons, of course.
Pros:
Each script is invoked in a separate clean session.
Even crashes do not stop the whole testing process.
Cons:
Invoking powershell.exe is somewhat slow.
Testing depends on exit codes but 0 does not always mean success.
None of the cons is mentioned is a question as a potential problem.
The demo script is below. It has been tested with PS v2 and v3. Script names
may include special characters like spaces, apostrophes, brackets, backticks,
dollars. One mentioned in comments requirement is ability to get script paths
in their code. With the proposed approach scripts can get their own path as
$MyInvocation.MyCommand.Path
# make a script list, use the full paths or explicit relative paths
$scripts = #(
'.\test1.ps1' # good name
'.\test 2.ps1' # with a space
".\test '3'.ps1" # with apostrophes
".\test [4].ps1" # with brackets
'.\test `5`.ps1' # with backticks
'.\test $6.ps1' # with a dollar
'.\test ''3'' [4] `5` $6.ps1' # all specials
)
# process each script in the list
foreach($script in $scripts) {
# make a command; mind &, ' around the path, and escaping '
$command = "& '" + $script.Replace("'", "''") + "'"
# invoke the command, i.e. the script in a separate process
powershell.exe -command $command
# check for the exit code (assuming 0 is for success)
if ($LastExitCode) {
# in this demo just write a warning
Write-Warning "Script $script failed."
}
else {
Write-Host "Script $script succeeded."
}
}
If you're on PowerShell 2.0 or higher, you can use jobs to do this. Each job runs in a separate PowerShell process e.g.:
$scripts = ".\script1.ps1", ".\script2.ps1"
$jobs = #()
foreach ($script in $scripts)
{
$jobs += Start-Job -FilePath $script
}
Wait-Job $jobs
foreach ($job in $jobs)
{
"*" * 60
"Status of '$($job.Command)' is $($job.State)"
"Script output:"
Receive-Job $job
}
Also, check out the PowerShell Community Extensions. It has a Test-Script command that can detect syntax errors in a script file. Of course, it won't catch runtime errors.
One tip for PowerShell V3 users: we (the PowerShell team) added a new API on the Runspace class called ResetRunspace(). This API resets the global variable table back to the initial state for that runspace (as well as cleaning up a few other things). What it doesn't do is clean out function definitions, types and format files or unload modules. This allows the API to be much faster. Also note that the Runspace has to have been created using an InitialSessionState object, not a RunspaceConfiguration instance. ResetRunspace() was added as part of the Workflow feature in V3 to support parallel execution efficiently in a script.
The two instances are totally separate, because they are two different processes. Generally, it is not the most efficient way to start a Powershell process for every script run. Depending on the number of scripts and how often you re-run them, it may be affecting your overall performance. If it's not, I would leave everything AS IS.
Another option would be to run in the same runspace (this is a correct word for it), but clean everything up every time. See this answer for a way to do it. Or use below extract:
$sysvars = get-variable | select -Expand name
function remove-uservars {
get-variable |
where {$sysvars -notcontains $_.name} |
remove-variable
}