在 Swift 中,如何查看 Process() 传递给 shell 的字符串?

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

我正在尝试使用 Process() 在 Swift 中启动任务,但我需要查看发送到 shell 的内容以进行调试。

我正在尝试发送以下命令:

gswin64c.exe -q -dNODISPLAY -dNOSAFER -c "(input.pdf) (r) file runpdfbegin pdfpagecount = quit"

如果我在使用 UNIX shell(bash、zsh 等)的环境中 运行 使用完全相同的命令,它 运行 没问题。然而,在 Windows 中使用 cmd.exe,它失败了,给出了以下错误信息:

Error: /undefined in ".

我怀疑 Swift 插入斜杠作为“转义”字符。有没有办法查看 Swift 发送到 shell 的字符串?

这是一个示例:

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)
}

作为后续,命令可以写在 Swift 中以便正确传递给 GhostScript 吗?


跟进:

似乎没有直接的方法来查看 Swift 发送到 shell 的内容。

但是,我能够解决眼前的问题。发送代码到 Windows 命令 shell 的消毒程序似乎在空格前插入了斜杠。我能够通过删除 PostScript 指令两边的引号(事实证明它们不是必需的)并将每个元素放在数组的单独成员中来解决这个问题:

task.arguments = [ "-q",
               "-dNODISPLAY",
               "-dNOSAFER",
               "-c",
               "(\(inputFile.path))",
               "(r)",
               "file",
               "runpdfbegin",
               "pdfpagecount",
               "=",
               "quit" ]

否则,如果您想查看整个工作示例:

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)
}

检查 swift-corelibs-foundation 中的代码后,我想我发现它如何在幕后修改 Windows 的参数。

Process.run中,首先构造一个command: [String](Line 495):

    var command: [String] = [launchPath]
    if let arguments = self.arguments {
      command.append(contentsOf: arguments)
    }

在你的情况下,它将是:

let command = [#"C:\Program Files\gs\gs9.56.1\bin\gswin64c.exe"#, "-q",
              "-dNODISPLAY",
              "-dNOSAFER",
              "-c",
              "\"(\(inputFile.path)) (r) file runpdfbegin pdfpagecount = quit\""]

然后在一大堆代码之后,它调用 quoteWindowsCommandLine 为 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 声明为 here(为简洁起见,我删除了注释):

private func quoteWindowsCommandLine(_ commandLine: [String]) -> String {
    func quoteWindowsCommandArg(arg: String) -> String {
        if !arg.contains(where: {" \t\n\"".contains([=13=])}) {
            return arg
        }
        var quoted = "\""
        var unquoted = arg.unicodeScalars

        while !unquoted.isEmpty {
            guard let firstNonBackslash = unquoted.firstIndex(where: { [=13=] != "\" }) 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: " ")
}

你可以 copy-paste 把它变成游乐场,和它一起玩。结果你的字符串变成了:

"C:\Program Files\gs\gs9.56.1\bin\gswin64c.exe" -q -dNODISPLAY -dNOSAFER -c "\"(/currentdir/input.pdf) (r) file runpdfbegin pdfpagecount = quit\""

显然 Windows 不需要引用最后一个参数。 quoteWindowsCommandLine 已经为您报价了。如果你只是说:

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))

不引用最后一个参数似乎也适用于 macOS。

另一个错误是您使用了 inputFile.path,它总是生成带有 / 的路径(参见 this)。您应该使用 URL:

的“文件系统表示”
inputFile.withUnsafeFileSystemRepresentation { pointer in
    task.arguments = ["-q",
                      "-dNODISPLAY",
                      "-dNOSAFER",
                      "-c",
                      "(\(String(cString: pointer!)) (r) file runpdfbegin pdfpagecount = quit"]
}

然后它似乎产生了一些看起来正确的东西:

"C:\Program Files\gs\gs9.56.1\bin\gswin64c.exe" -q -dNODISPLAY -dNOSAFER -c "(/currentdir/input.pdf) (r) file runpdfbegin pdfpagecount = quit"

(我没有 Windows 机器)