Search code examples
c#shell7zipash

Why does a Process.Start shell command fail with "unterminated quoted string" in Linux?


The problem

I am trying to run 7z from C# code to extract some files. The command works under Windows or if I just run it from the shell directly, but not from code. Maybe it's not the exact same, and I don't realize it.

So I'm doing this:

//Unpack dump with 7z
StringBuilder argument = new($"\"{Config.zipPath}\" x \"{dumpPath}\" -so | \"{Config.zipPath}\" x -y -si -ttar");
foreach (string file in new[] { "vn", "vn_titles" /* and other strings */ }) {
    argument.Append($" db/{file} db/{file}.header");
}
Process.Start(Config.shell, string.Format(Config.arguments, argument)).WaitForExit();

The config file is a list of key=value pairs that get loaded into the static Config class. So, it works for Windows:

shell=cmd.exe
zipPath=C:/Program Files/7-Zip-Zstandard/7z.exe
arguments=/C "{0}"

but not for Linux:

shell=ash
zipPath=7z
arguments=-c '{0}'

The ash command there fails with an error:
x: line 0: syntax error: unterminated quoted string


Some things I've tried / checked

  • The C# code runs without error, at least up until it needs to access the unpacked files which aren't there.
  • Config.shell is ash
  • string.Format(Config.arguments, argument) is -c '"7z" x "dump.tar.zst" -so | "7z" x -y -si -ttar db/vn db/vn.header db/vn_titles db/vn_titles.header db/releases db/releases.header db/releases_titles db/releases_titles.header db/releases_vn db/releases_vn.header db/vn_length_votes db/vn_length_votes.header db/tags db/tags.header db/tags_parents db/tags_parents.header db/tags_vn db/tags_vn.header'
  • Based on the previous two points, I would assume the command that gets executed is the concatenation of those two, that is, ash -c '"7z" x "dump.tar.zst" -so | "7z" x ... ', and that command, ran manually, works as expected. So I'm really having trouble figuring out where the problem comes from.

The non-ideal found solution

The answer provided by knittl should be correct, as far as I understand. Whether Windows or Linux, the shell should be called with 2 arguments - -c (or /C for Windows' cmd) and then the command I want to run, which is correctly the argument string. This is generally the simpler method. But it doesn't work under Windows.

cmd does not seem to support spaces in path names inside the argument to \C, at all. Not enclosing them in quotation marks fails to recognize them as a single string, and enclosing them in quotation marks makes the path invalid, as quotation marks are not part of the path.

powershell doesn't have this problem, but it has a limit of 256 characters for the argument of -c, also making my command unusable.

So the solution is to detect which OS I'm on via Environment.OSVersion.Platform, and either pass the arguments as a string array for Unix, or use the special feature of the cmd /C command for Win32NT, where it doesn't actually parse the whole command if the first character of the command is a ", but instead just strips that and the last character if it's also a ", saving me from escaping those characters inside the command, which is the only reason my initial version worked at all.


Solution

  • -c '...' must be two arguments exactly (-c and whatever is inside the quotes). I'm not sure how the Process.Start(string, string) overloads splits the second parameter into command line arguments, but from your question it looks like it is not splitting them as one (or your shell) would expect :)

    You want to call the overload Start(string, IEnumerable<string>):

    Process.Start(Config.shell, new[]{ "-c",  argument })