Home > Net >  In Swift, how can I see the string that Process() passes to the shell?
In Swift, how can I see the string that Process() passes to the shell?

Time:04-14

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)

  • Related