I am trying to use Process()
to start a task in Swift, but I need to see what is being sent to the shell for debugging purposes.
I am trying to send the following command:
gswin64c.exe -q -dNODISPLAY -dNOSAFER -c "(input.pdf) (r) file runpdfbegin pdfpagecount = quit"
If I run the very same command in an environment that uses a UNIX shell (bash, zsh, etc.), it runs fine. In Windows using cmd.exe, however, it fails, giving the following error message:
Error: /undefined in ".
I suspect that Swift is inserting slashes as “escape” characters. Is there a way to see the string that Swift is sending to the shell?
Here is a sample:
import Foundation
let inputFile = URL(fileURLWithPath: "input.pdf")
let task = Process()
// In MacOS or Linux, obviously, we would use the appropriate path to 'gs'.
// Use gswin32c.exe if you have the 32-bit version of Ghostscript in Windows.
task.executableURL = URL(fileURLWithPath: #"C:\Program Files\gs\gs9.56.1\bin\gswin64c.exe"#)
// The following works when the shell is bash, zsh, or similar, but not with cmd
task.arguments = ["-q",
"-dNODISPLAY",
"-dNOSAFER",
"-c",
"\"(\(inputFile.path)) (r) file runpdfbegin pdfpagecount = quit\""]
let stdout = Pipe()
let stderr = Pipe()
task.standardOutput = stdout
task.standardError = stderr
do {
try task.run()
} catch {
print(error)
exit(1)
}
task.waitUntilExit()
extension String {
init?(pipe: Pipe) {
guard let data = try? pipe.fileHandleForReading.readToEnd() else {
return nil
}
guard let result = String(data: data, encoding: .utf8) else {
return nil
}
self = result
}
}
if let stdoutText = String(pipe: stdout) {
print(stdoutText)
}
if let stderrText = String(pipe: stderr) {
print(stderrText)
}
As a follow up, can the command be written in Swift so that it gets passed on to GhostScript correctly?
Follow up:
There does not appear to be a straightforward way to see what Swift sends to the shell.
However, I was able to solve my immediate problem. It seems that the sanitizer sending the code to the Windows command shell inserts slashes in front of the spaces. I was able to work around the issue by removing the quotation marks on either side of the PostScript instructions (it turns out they are not necessary), and placing each element in a separate member of the array:
task.arguments = [ "-q",
"-dNODISPLAY",
"-dNOSAFER",
"-c",
"(\(inputFile.path))",
"(r)",
"file",
"runpdfbegin",
"pdfpagecount",
"=",
"quit" ]
Or else, if you prefer to see the entire working example:
import Foundation
let inputFile = URL(fileURLWithPath: "input.pdf")
let task = Process()
// In MacOS or Linux, obviously, we would use the appropriate path to 'gs'.
// Use gswin32c.exe if you have the 32-bit version of Ghostscript in Windows.
task.executableURL = URL(fileURLWithPath: #"C:\Program Files\gs\gs9.56.1\bin\gswin64c.exe"#)
print(inputFile.path)
task.arguments = [ "-q",
"-dNODISPLAY",
"-dNOSAFER",
"-c",
"(\(inputFile.path))",
"(r)",
"file",
"runpdfbegin",
"pdfpagecount",
"=",
"quit" ]
let stdout = Pipe()
let stderr = Pipe()
task.standardOutput = stdout
task.standardError = stderr
do {
try task.run()
} catch {
print(error)
exit(1)
}
task.waitUntilExit()
extension String {
init?(pipe: Pipe) {
guard let data = try? pipe.fileHandleForReading.readToEnd() else {
return nil
}
guard let result = String(data: data, encoding: .utf8) else {
return nil
}
self = result
}
}
if let stdoutText = String(pipe: stdout) {
print(stdoutText)
}
if let stderrText = String(pipe: stderr) {
print(stderrText)
}
CodePudding user response:
After checking the code in swift-corelibs-foundation, I think I found how it modifies your arguments for Windows under the hood.
In Process.run
, it first constructs a command: [String]
(Line 495):
var command: [String] = [launchPath]
if let arguments = self.arguments {
command.append(contentsOf: arguments)
}
In your case, it would be:
let command = [#"C:\Program Files\gs\gs9.56.1\bin\gswin64c.exe"#, "-q",
"-dNODISPLAY",
"-dNOSAFER",
"-c",
"\"(\(inputFile.path)) (r) file runpdfbegin pdfpagecount = quit\""]
Then after a whole bunch of code, it calls quoteWindowsCommandLine
to create a command for the Windows shell (Line 656):
try quoteWindowsCommandLine(command).withCString(encodedAs: UTF16.self) { wszCommandLine in
try FileManager.default._fileSystemRepresentation(withPath: workingDirectory) { wszCurrentDirectory in
try szEnvironment.withCString(encodedAs: UTF16.self) { wszEnvironment in
if !CreateProcessW(nil, UnsafeMutablePointer<WCHAR>(mutating: wszCommandLine),
quoteWindowsCommandLine
is declared here (I've removed the comments for brevity):
private func quoteWindowsCommandLine(_ commandLine: [String]) -> String {
func quoteWindowsCommandArg(arg: String) -> String {
if !arg.contains(where: {" \t\n\"".contains($0)}) {
return arg
}
var quoted = "\""
var unquoted = arg.unicodeScalars
while !unquoted.isEmpty {
guard let firstNonBackslash = unquoted.firstIndex(where: { $0 != "\\" }) else {
let backslashCount = unquoted.count
quoted.append(String(repeating: "\\", count: backslashCount * 2))
break
}
let backslashCount = unquoted.distance(from: unquoted.startIndex, to: firstNonBackslash)
if (unquoted[firstNonBackslash] == "\"") {
quoted.append(String(repeating: "\\", count: backslashCount * 2 1))
quoted.append(String(unquoted[firstNonBackslash]))
} else {
quoted.append(String(repeating: "\\", count: backslashCount))
quoted.append(String(unquoted[firstNonBackslash]))
}
unquoted.removeFirst(backslashCount 1)
}
quoted.append("\"")
return quoted
}
return commandLine.map(quoteWindowsCommandArg).joined(separator: " ")
}
You can copy-paste this into a playground, and play around with it. It turns out that your string got turned into:
"C:\Program Files\gs\gs9.56.1\bin\gswin64c.exe" -q -dNODISPLAY -dNOSAFER -c "\"(/currentdir/input.pdf) (r) file runpdfbegin pdfpagecount = quit\""
Apparently the last argument doesn't need to be quoted on Windows. quoteWindowsCommandLine
does the quoting for you already. If you just say:
let command = [#"C:\Program Files\gs\gs9.56.1\bin\gswin64c.exe"#, "-q",
"-dNODISPLAY",
"-dNOSAFER",
"-c",
"(\(inputFile.path)) (r) file runpdfbegin pdfpagecount = quit"]
print(quoteWindowsCommandLine(command))
Not quoting the last argument seems to work on macOS too.
Another mistake is that you used inputFile.path
, which always produces paths with /
(see this). You should use the "file system representation" of the URL
:
inputFile.withUnsafeFileSystemRepresentation { pointer in
task.arguments = ["-q",
"-dNODISPLAY",
"-dNOSAFER",
"-c",
"(\(String(cString: pointer!)) (r) file runpdfbegin pdfpagecount = quit"]
}
Then it seems to produce something that looks right:
"C:\Program Files\gs\gs9.56.1\bin\gswin64c.exe" -q -dNODISPLAY -dNOSAFER -c "(/currentdir/input.pdf) (r) file runpdfbegin pdfpagecount = quit"
(I don't have a Windows machine)