Sources/XCMetricsUtils/Shell.swift (73 lines of code) (raw):

// Copyright (c) 2020 Spotify AB. // // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. import Foundation public enum ShellError: Error { case statusError(String, Int32) } extension ShellError: LocalizedError { public var errorDescription: String? { switch self { case .statusError(let string, _): return string } } } protocol PipeLike {} extension FileHandle: PipeLike {} extension Pipe: PipeLike {} public typealias ShellOutFunction = (String, [String], String?, [String: String]?) throws -> String public func shellExec(_ cmd: String, args: [String] = [], inDir dir: String? = nil, environment: [String: String]? = nil) throws { try shellInternal(cmd, args: args, stdout: nil, stderr: nil, inDir: dir, environment: environment) } public func shellGetStdout(_ cmd: String, args: [String] = [], inDir dir: String? = nil, environment: [String: String]? = nil) throws -> String { let pipe = Pipe() try shellInternal(cmd, args: args, stdout: pipe, stderr: nil, inDir: dir, environment: environment) let handle = pipe.fileHandleForReading let data = handle.readDataToEndOfFile() return String(data: data, encoding: .utf8)?.trim() ?? "" } private func which(_ cmd: String) throws -> String { return try shellGetStdout("/usr/bin/which", args: [cmd]) } private func shellInternal(_ cmd: String, args: [String] = [], stdout: PipeLike?, stderr: PipeLike?, inDir dir: String? = nil, environment: [String: String]? = nil) throws { let absCmd = try cmd.starts(with: "/") ? cmd : which(cmd) let errorHandle = Pipe() let task = Process() if let env = environment { task.environment = env } task.launchPath = absCmd task.arguments = args task.standardOutput = stdout ?? FileHandle.nullDevice task.standardError = stderr ?? errorHandle if let dir = dir { task.currentDirectoryPath = dir } task.launch() task.waitUntilExit() if task.terminationStatus != 0 { if stderr != nil { // Error stream was captured so cannot inspect its content throw ShellError.statusError("Failed command", task.terminationStatus) } let errorData = errorHandle.fileHandleForReading.readDataToEndOfFile() let errorString = String(data: errorData, encoding: .utf8)?.trim() ?? "No error returned from the process." throw ShellError.statusError( "status \(task.terminationStatus): \(errorString)", task.terminationStatus ) } } extension String { public func trim() -> String { func trim(_ separator: String) -> String { var E = endIndex while String(self[startIndex..<E]).hasSuffix(separator) && E > startIndex { E = index(before: E) } return String(self[startIndex..<E]) } if hasSuffix("\r\n") { return trim("\r\n") } else if hasSuffix("\n") { return trim("\n") } else { return self } } }