Reputation: 2751
I'm experiencing some differences in Process.Start()
in a .NET Core 2.2 project. You can find the full source in this gist: https://gist.github.com/jchristn/5a2a301baedeed787a2e57cd528e46d6
I have a method for starting processes:
private static void ExecuteShell(
string filename,
string args,
bool useShellExecute,
bool redirectStdOut,
bool redirectStdErr,
out int returnCode,
out string consoleOutput)
{
returnCode = 0;
consoleOutput = null;
if (String.IsNullOrEmpty(filename)) throw new ArgumentNullException(nameof(filename));
// fileName, i.e. "cmd.exe"
// args, i.e. "/c dir /w"
Process process = new Process();
process.StartInfo.FileName = filename;
process.StartInfo.Arguments = args;
process.StartInfo.UseShellExecute = useShellExecute;
process.StartInfo.RedirectStandardOutput = redirectStdOut;
process.StartInfo.RedirectStandardError = redirectStdErr;
process.Start();
if (process.StartInfo.RedirectStandardOutput) consoleOutput = process.StandardOutput.ReadToEnd();
process.WaitForExit();
returnCode = process.ExitCode;
}
And the caller looks like this:
while (true)
{
try
{
Console.WriteLine("");
Console.WriteLine("Example: cmd.exe /c dir /w");
Console.Write("Command: ");
string userInput = Console.ReadLine();
if (String.IsNullOrEmpty(userInput)) break;
string[] parts = userInput.Split(new char[] { ' ' }, 2);
string filename = parts[0];
string arg = null;
if (parts.Length > 1) arg = parts[1];
bool useShellExecute = InputBoolean(" Use shell execute : ", false);
bool redirectStdOut = InputBoolean(" Redirect stdout : ", false);
bool redirectStdErr = InputBoolean(" Redirect stderr : ", false);
int returnCode;
string consoleOutput;
ExecuteShell(
filename,
arg,
useShellExecute,
redirectStdOut,
redirectStdErr,
out returnCode, out consoleOutput);
Console.WriteLine("Return code : " + returnCode);
Console.WriteLine("Console output : " + consoleOutput);
}
catch (Exception e)
{
Console.WriteLine("Exception: " + Environment.NewLine + SerializeJson(e, true));
}
}
The simplest way to reproduce the larger issue I'm having is this. Assume there is a file on the file system and I want to type testfile > testfile2
, i.e. to pipe to another file.
On Windows, if I use cmd.exe /c type testfile > testfile2
it works great (with the three Boolean values set to false
).
i.e.
C:\Code\ExecuteShell\ExecuteShell\bin\Debug\netcoreapp2.2>dotnet ExecuteShell.dll
Example: cmd.exe /c dir /w
Command: cmd.exe /c type testfile > testfile2
Use shell execute : [y/N]?
Redirect stdout : [y/N]?
Redirect stderr : [y/N]?
Return code : 0
Console output :
Example: cmd.exe /c dir /w
Command:
C:\Code\ExecuteShell\ExecuteShell\bin\Debug\netcoreapp2.2>dir
Volume in drive C is OS
Volume Serial Number is 72E2-466A
Directory of C:\Code\ExecuteShell\ExecuteShell\bin\Debug\netcoreapp2.2
... portions removed ...
04/09/2020 05:41 PM 15 testfile
04/09/2020 05:41 PM 15 testfile2
When I try this on Ubuntu 14.04 it fails.
~/code/ExecuteShell/ExecuteShell/bin/Debug/netcoreapp2.2/publish$ dotnet ExecuteShell.dll
Example: cmd.exe /c dir /w
Command: cat testfile > testfile2
Use shell execute : [y/N]?
Redirect stdout : [y/N]?
Redirect stderr : [y/N]?
Hello, world!
cat: >: No such file or directory
cat: testfile2: No such file or directory
Return code : 1
Console output :
If I try it with useShellExecute
set to true
, I get this strange xdg-open
issue:
~/code/ExecuteShell/ExecuteShell/bin/Debug/netcoreapp2.2/publish$ dotnet ExecuteShell.dll
Example: cmd.exe /c dir /w
Command: cat testfile > testfile2
Use shell execute : [y/N]? y
Redirect stdout : [y/N]?
Redirect stderr : [y/N]?
xdg-open: unexpected argument 'testfile'
Try 'xdg-open --help' for more information.
Return code : 1
Console output :
Any idea how to make this work?
Upvotes: 1
Views: 4129
Reputation: 2010
Process
is a fairly opaque and convoluted abstraction and unfortunately it has lots of undocumented pitfalls, mostly because it attempts to emulate Windows-like behaviour on Unix. The only way to figure out exactly what it's doing is to read the latest and greatest .NET source code.
ProcessStartInfo.UseShellExecute
behaves weird on Unix-like systems:
It does the following in order:
Attempts to resolve absolute executable path. If the path is "rooted" (starts with /), it uses it as is. If the path is a URI, and that URI describes a local path on the system, it uses the local path as the path. If the filename is relative, it resolves it into an absolute path by first searching the working directory, and then by searching the current PATH
. Finally, it checks the file at the path has the x (Execute) bit set, and if it doesn't, throws it away.
If the absolute path was resolved, and the file appears to be executable, it attempts to execute it directly.
If the file couldn't be executed directly, attempt to find the default program that can be used to open the file. This is operating system dependent. On Linux, it searches for any of "xdg-open", "gnome-open", "kfmclient" in the path. On FreeBSD, it tries to use "/usr/local/bin/open". On OSX, it tries to use "/usr/bin/open".
Finally it runs the default program which takes the original filename as the first argument and the rest of the arguments after that.
So, this is why you get that xdg-open
error. It's attempting to open the file with a Window Manager to simulate how Windows operates. For this reason UseShellExecute = false
is almost always preferable on Unix.
As for the arguments issue, it causes problems because .NET is splitting the string into an argv
array using its own little state machine tokenizer:
This notably ignores single quotes ('
), only recognizing double quotes ("
). Again, it's an annoying abstraction over what's actually passed to the process, the argv
array.
It's much better to ignore ProcessStartInfo.Arguments
entirely and instead add individual arguments with ProcessStartInfo.ArgumentList.Add()
, which will cause them to be translated directly into the process argv
array.
Upvotes: 1
Reputation: 76409
In this case, you're confusing the use of the word “shell” in its Unix context (a command line interpreter) with its Windows and C# use:
The word "shell" in this context (UseShellExecute) refers to a graphical shell (similar to the Windows shell) rather than command shells (for example, bash or sh) and lets users launch graphical applications or open documents.
So useShellExecute
actually means that you can give the program a document of some type and expect it to be opened by a suitable program. That's what xdg-open
does, so that's probably why C# invokes it.
In your case, the command you want to run is sh -c 'cat testfile > testfile2'
. That's the equivalent of your cmd
invocation. However, your code won't work if you do that now because you split on whitespace. So you'll end up with the arguments sh
, -c
, 'cat
, testfile
, >
, and testfile2'
. Unlike cmd
, which is responsible for its own argument processing, sh
does not concatenate its commands with whitespace, and this won't work.
If you were passing this as an array of arguments, you'd want your arguments to be sh
, -c
, and cat testfile > testfile2
; that is, the entire string you want to pass to the shell should be one complete argument.
Upvotes: 4