-
-
Save griotspeak/b23670aaa422581226877e3fdc147864 to your computer and use it in GitHub Desktop.
SwiftInSwift - A poor man's meta programming tool for Swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//----------------------------------------------------------------------------- | |
// main.swift | |
// SwiftInSwift | |
//----------------------------------------------------------------------------- | |
// Compile this tool and use it as a Build Phase (before the compilation phase). | |
// In the added Run Script Build Phase, you call it like this: | |
// /path/to/SwiftInSwift . | |
// | |
// The "." is just the directory where it will search for swift files. | |
// The args should be directories or paths to individual swift files. | |
// Note that this tool has a lot of rough edges, and I accept no responsibility | |
// for anything that might happen to your code or computer etc while using it. | |
// License is MIT and/or BSD or something ... Improvements are welcome! | |
// Please let us know @bitCycle if you make a similar but better tool based on | |
// or inspired by this. | |
import Foundation | |
let swiftFiles = { () -> [String : [String]] in | |
var swiftFiles = [String : [String]]() | |
let fm = FileManager.default | |
let currDirPath = fm.currentDirectoryPath | |
func addSwiftFile(_ swiftFileOrDirectoryPath: String) { | |
let path = NSString(string: currDirPath + "/" + | |
swiftFileOrDirectoryPath).standardizingPath | |
if swiftFiles[path] != nil { | |
exitWithError( | |
"error: input file mentioned more than once: \(path)") } | |
var isDir: ObjCBool = true | |
let fileExist = fm.fileExists(atPath: path, isDirectory: &isDir) | |
guard fileExist == true | |
else { exitWithError("error: no such file or directory: \(path)") } | |
if isDir.boolValue == true { | |
for swiftFile in | |
cmdFind.execute(arguments: [".", "-name", "*.swift"]) | |
.validNonEmptyResult?.components(separatedBy: "\n") ?? [] | |
{ addSwiftFile(swiftFile) } | |
return | |
} | |
if path.hasSuffix(".swift") == false { | |
exitWithError("error: input file is not a Swift file: \(path)") } | |
guard let codeLines = try? [String](contentsOfFile: path) | |
else { exitWithError("error: can't read file: \(path)") } | |
swiftFiles[path] = codeLines | |
} | |
for arg in CommandLine.arguments.dropFirst() { addSwiftFile(arg) } | |
if swiftFiles.isEmpty { | |
exitWithError([ | |
"error: no input files", | |
"usage: \(appName) <Swift file or directory> ..." | |
].joined(separator: "\n") | |
) | |
} | |
return swiftFiles | |
}() | |
//----------------------------------------------------------------------------- | |
let concatenatedDefBlocks = { () -> [String] in | |
var concatenatedDefBlocks = [String]() | |
enum State { | |
case lookingForDefBlockPrefix | |
case lookingForDefBlockSuffix | |
} | |
var state = State.lookingForDefBlockPrefix | |
for (swiftFilePath, lines) in swiftFiles { | |
for lineIndex in lines.indices { | |
let line = lines[lineIndex] | |
switch state { | |
case .lookingForDefBlockPrefix: | |
if line == defBlockPrefix { | |
state = State.lookingForDefBlockSuffix | |
concatenatedDefBlocks.append( | |
"// DefBlock from \(swiftFilePath):\(lineIndex):" | |
) | |
} | |
else if line == defBlockSuffix { | |
exitWithError( | |
path: swiftFilePath, line: lineIndex+1, | |
message: "unmatched close-DefBlock") | |
} | |
case .lookingForDefBlockSuffix: | |
if line == defBlockSuffix { | |
state = State.lookingForDefBlockPrefix | |
} | |
else if line == defBlockPrefix { | |
exitWithError( | |
path: swiftFilePath, line: lineIndex+1, | |
message: "unmatched (nested) open-DefBlock") | |
} | |
else { | |
concatenatedDefBlocks.append(line) | |
} | |
} | |
} | |
} | |
// NOTE: Simply looks for line == defBlockPrefix/Suffix, which implies: | |
// - Fails to detect eg " // DEF-{", "// }-DEF " and "// }-DEF blabla". | |
// - Does detect a DefBlock that has been block-commented out. | |
return concatenatedDefBlocks | |
}() | |
//----------------------------------------------------------------------------- | |
struct Location { | |
let path: String | |
let range: CountableRange<Int> | |
} | |
let locationsForPrintExpression = { () -> [String : [Location]] in | |
var locationsForPrintExpression = [String : [Location]]() | |
enum State { | |
case lookingForPrintBlockPrefix | |
case lookingForPrintBlockSuffix(printExpr: String, start: Int) | |
} | |
var state = State.lookingForPrintBlockPrefix | |
for (swiftFilePath, lines) in swiftFiles { | |
for lineIndex in lines.indices { | |
let line = lines[lineIndex] | |
switch state { | |
case .lookingForPrintBlockPrefix: | |
if line.hasPrefix(printBlockPrefix) { | |
if let printExpression = | |
line.removingRequiredPrefix(printBlockPrefix)? | |
.trimmingCharacters(in: .whitespaces), | |
printExpression.isEmpty == false | |
{ | |
state = State.lookingForPrintBlockSuffix( | |
printExpr: printExpression, start: lineIndex) | |
} else { | |
exitWithError(path: swiftFilePath, line: lineIndex+1, | |
message: "open-PrintBlock has no PrintExpression") | |
} | |
} | |
else if line.hasPrefix(printBlockSuffix) { | |
exitWithError( | |
path: swiftFilePath, line: lineIndex+1, | |
message: "unmatched close-PrintBlock") | |
} | |
case let .lookingForPrintBlockSuffix(printExpr, startLineIndex): | |
if line == printBlockSuffix { | |
let location = Location( | |
path: swiftFilePath, | |
range: (startLineIndex ..< lineIndex) | |
) | |
if locationsForPrintExpression[printExpr]? | |
.append(location) == nil | |
{ | |
locationsForPrintExpression[printExpr] = [location] | |
} | |
state = State.lookingForPrintBlockPrefix | |
} | |
else if line.hasPrefix(printBlockPrefix) { | |
exitWithError( | |
path: swiftFilePath, line: lineIndex+1, | |
message: "unmatched (nested) open-PrintBlock") | |
} | |
} | |
} | |
} | |
if locationsForPrintExpression.isEmpty { | |
print("No PrintExpressions; Nothing to do. Bye!") | |
exit(0) | |
} | |
// NOTE: ~ The same things as for defBlocks above. | |
return locationsForPrintExpression | |
}() | |
//----------------------------------------------------------------------------- | |
let codeLinesForExpression: [String : [String]] = { | |
var exprEvalScript = [ | |
printExpressionEvaluatorScript.shebangLine, | |
"//-----------------------------------------------------", | |
"// \(printExpressionEvaluatorScript.name)", | |
"// Generated and called by \(appName) to evaluate", | |
"// the PrintExpressions of the PrintBlocks.", | |
"//-----------------------------------------------------", | |
"", | |
"//---------------------------------------------------------------", | |
"// Imports:", | |
"//---------------------------------------------------------------", | |
"import Foundation"] | |
+ concatenatedDefBlocks.filter({ $0.hasPrefix("import") == true }) | |
exprEvalScript += [ | |
"", | |
"//---------------------------------------------------------------", | |
"// DefBlocks:", | |
"//---------------------------------------------------------------" | |
] | |
+ concatenatedDefBlocks.filter({ $0.hasPrefix("import") == false }) | |
// We'll add a function that will be used to print the result of each | |
// (unique) PrintExpression to stdout. First make sure there's nothing | |
// in the user's code with the same name: | |
let nameOfPrintingFunc = printExpressionEvaluatorScript.printFuncName | |
for line in exprEvalScript + locationsForPrintExpression.keys { | |
if line.contains(nameOfPrintingFunc) { | |
exitWithError( | |
"error: DefBlocks and PrintBlocks can't contain: " | |
+ "'\(nameOfPrintingFunc)'.") | |
} | |
} | |
exprEvalScript += [ | |
"//----------------------------------------------------------------", | |
"// Handle errors by reporting to stderr and exit with exit code 1.", | |
"//----------------------------------------------------------------", | |
"func exitWithError(_ message: String) -> Never {", | |
" struct StderrOutputStream: TextOutputStream {", | |
" public mutating func write(_ s: String) {", | |
" fputs(s.cString(using: .utf8), stderr)", | |
" }", | |
" }", | |
" var errStream = StderrOutputStream()", | |
" print(message, to: &errStream)", | |
" exit(1)", | |
"}", | |
"", | |
"//---------------------------------------------------------------", | |
"// This function is called for each (unique) PrintExpression:", | |
"//---------------------------------------------------------------", | |
"func \(nameOfPrintingFunc)(", | |
" _ exprString: String,", | |
" _ exprResult: [String])", | |
"{", | |
" print(\"// PrintExpression: \" + exprString)", | |
" print(\"// Number of lines: \\(exprResult.count)\")", | |
" if exprResult.isEmpty { return }", | |
" let exprResultStr = exprResult.joined(separator: \"\\n\")", | |
" if exprResultStr.components(separatedBy: \"\\n\").count !=", | |
" exprResult.count { exitWithError(exprString) }", | |
" print(exprResultStr)", | |
"}", | |
"", | |
"//---------------------------------------------------------------", | |
"// Calling \(nameOfPrintingFunc) for each (unique) PrintExpression:", | |
"//---------------------------------------------------------------", | |
] | |
// A map from script line # to expression, used later for error reporting: | |
var expressionForScriptLineIndex = [Int : String]() | |
// Append a call to the above printing func for each expression: | |
for expressionString in locationsForPrintExpression.keys { | |
var expr = expressionString | |
guard expr.contains("\\n") == false && expr.contains("\\r") == false | |
else { | |
let dest = locationsForPrintExpression[expr]!.first! | |
exitWithError( | |
path: dest.path, | |
line: dest.range.lowerBound+1, | |
message: "PrintExpression cannot contain \\n or \\r.") } | |
let quotedAndEscapedExpr = expr.debugDescription | |
exprEvalScript.append( | |
"\(nameOfPrintingFunc)(\(quotedAndEscapedExpr), \(expr))") | |
expressionForScriptLineIndex[exprEvalScript.count] = expressionString | |
} | |
// Make tempDirPath: | |
guard let _ = cmdBash | |
.execute(arguments: ["-c", "mkdir -p \(tempDirPath)"]) | |
.validResult | |
else { exitWithError("error: couldn't make temp dir: \(tempDirPath)")} | |
let scriptPath = tempDirPath + "/" + printExpressionEvaluatorScript.name | |
guard (try? exprEvalScript.write(toFile: scriptPath)) != nil | |
else { exitWithError("error: couldn't write \(scriptPath)") } | |
guard let _ = cmdBash | |
.execute(arguments: ["-c", "chmod +x '\(scriptPath)'"]) | |
.validResult | |
else { exitWithError("error: couldn't make \(scriptPath) executable") } | |
// Launch the expression evaluator script: | |
let scriptResult = cmdBash.execute(arguments: ["-c", scriptPath]) | |
guard let validScriptResult = scriptResult | |
.validNonEmptyResult?.components(separatedBy: "\n") | |
else { | |
// Handle the kind of error in which the expression is reported: | |
if let dest = locationsForPrintExpression[scriptResult.stdErr]? | |
.first | |
{ | |
exitWithError( | |
path: dest.path, | |
line: dest.range.lowerBound+1, | |
message: "result of PrintExpression contains " + | |
"illegal characters like eg \\n") | |
// TODO: If more than this error need to be handled. | |
// Move the message part into the script, as a | |
// second stdErr line or something. | |
} | |
// If scriptResult.stdErr was not a PrintExpression then it might | |
// be something like this: | |
// /path/to/printExprEvalScript:58:19: error: <msg> | |
// In which case we could try and get to the expr by using the | |
// expressionForScriptLineIndex from above: | |
if let stdErrSuffix = scriptResult.stdErr | |
.removingRequiredPrefix(scriptPath + ":"), | |
let splitRange = stdErrSuffix.range(of: ": error: ") | |
{ | |
let nums = stdErrSuffix.substring(with: | |
stdErrSuffix.startIndex ..< splitRange.lowerBound) | |
.components(separatedBy:":") | |
let msg = stdErrSuffix.substring(with: | |
splitRange.upperBound ..< stdErrSuffix.endIndex) | |
if nums.count == 2, | |
let lineIndex = Int(nums[0]), | |
let expr = expressionForScriptLineIndex[lineIndex], | |
let dest = locationsForPrintExpression[expr]?.first | |
{ | |
exitWithError(path: dest.path, | |
line: dest.range.lowerBound+1, | |
message: msg) | |
} | |
} | |
exitWithError(scriptResult.stdErr) | |
} | |
// validScriptResult now has consecutive blocks of lines like these: | |
// "// PrintExpression: <expression-string>" | |
// "// Number of lines: <line-count for output>" | |
// "<1st line of ouput>" | |
// "<2nd line of ouput>" | |
// ... | |
var codeLinesForExpression = [String : [String]]() | |
var scriptLineIndex = 0 | |
enum State { | |
case lookingForPrintExpression | |
case lookingForNumberOfLines(forPrintExpr: String) | |
case collectingLines(forPrintExpr: String, linesToGo: Int) | |
} | |
var state = State.lookingForPrintExpression | |
// If the output cannot be parsed as expected, the scriptResult (stdout) | |
// will be written to a file which the error report will point to. | |
func exitWithErrorInScriptResult(line: Int, message: String) -> Never { | |
let resultPath = tempDirPath + | |
"/" + printExpressionEvaluatorScript.resultFilename | |
guard (try? validScriptResult.write(toFile: resultPath)) != nil | |
else { exitWithError("error: couldn't write file: \(resultPath)") } | |
exitWithError(path: resultPath, line: line, message: message) | |
} | |
// Parse the scriptResult. | |
for scriptResultLineIndex in validScriptResult.indices { | |
let resultLine = validScriptResult[scriptResultLineIndex] | |
switch state { | |
case .lookingForPrintExpression: | |
guard let printExpr = resultLine.removingRequiredPrefix( | |
"// PrintExpression: ") | |
else { | |
exitWithErrorInScriptResult( | |
line: scriptResultLineIndex+1, | |
message: "expected PrintExpression") } | |
state = .lookingForNumberOfLines(forPrintExpr: printExpr) | |
case let .lookingForNumberOfLines(printExpr): | |
guard let numLinesStr = | |
resultLine.removingRequiredPrefix("// Number of lines: "), | |
let numLines = Int(numLinesStr), | |
numLines >= 0 && numLines < 100_000 | |
else { exitWithErrorInScriptResult( | |
line: scriptResultLineIndex+1, | |
message: "error: expected number of " + | |
"lines (>=0, <100_000), not: \(resultLine)") } | |
if numLines == 0 { | |
state = .lookingForPrintExpression | |
let emptyResultLine = "// (Result of PrintExpression was [])" | |
if codeLinesForExpression[printExpr]? | |
.append(emptyResultLine) == nil | |
{ | |
codeLinesForExpression[printExpr] = [emptyResultLine] | |
} | |
} else { | |
state = .collectingLines(forPrintExpr: printExpr, | |
linesToGo: numLines) | |
} | |
case let .collectingLines(printExpr, linesToGo): | |
if codeLinesForExpression[printExpr]?.append(resultLine) == nil { | |
codeLinesForExpression[printExpr] = [resultLine] | |
} | |
if linesToGo - 1 > 0 { | |
state = .collectingLines(forPrintExpr: printExpr, | |
linesToGo: linesToGo - 1) | |
} else { | |
state = .lookingForPrintExpression | |
} | |
} | |
} | |
guard case .lookingForPrintExpression = state | |
else { exitWithErrorInScriptResult( | |
line: validScriptResult.count, | |
message: "unexpected end of file") } | |
return codeLinesForExpression | |
}() | |
//----------------------------------------------------------------------------- | |
let newSwiftFiles: [String : [String]] = { | |
// Need a map from swiftFilePath to a sorted list of its PrintBlocks. | |
struct PrintBlock { | |
let expression: String | |
let location: Location | |
} | |
var printBlocksForPath = [String : [PrintBlock]]() | |
for (expr, locations) in locationsForPrintExpression { | |
for location in locations { | |
let printBlock = PrintBlock(expression: expr, location: location) | |
if printBlocksForPath[location.path]?.append(printBlock) == nil { | |
printBlocksForPath[location.path] = [printBlock] | |
} | |
} | |
} | |
var newSwiftFiles = [String : [String]]() | |
for (path, pBlocks) in printBlocksForPath { | |
// Perhaps the PrintBlocks will always be sorted in ascending | |
// line ranges, but I'm not sure, so sort them anyway. | |
let sortedPrintBlocks = pBlocks.sorted(by: { | |
$0.location.range.lowerBound < $1.location.range.lowerBound | |
}) | |
guard let oldCodeLines = swiftFiles[path] | |
else { exitWithError("error: \(path) not in swiftFiles!??!?") } | |
var newCodeLines = [String]() | |
var lastBlockEnd = 0 | |
var unchangedPrintBlocks = 0 | |
for pb in sortedPrintBlocks { | |
let currStart = pb.location.range.lowerBound + 1 | |
let oldSection = oldCodeLines[lastBlockEnd ..< currStart] | |
newCodeLines.append(contentsOf: oldSection) | |
guard let printExprResult = codeLinesForExpression[pb.expression] | |
else { exitWithError("error: \(pb.expression) not in " | |
+ "codeLinesForExpression!?!?!?") } | |
if printExprResult.elementsEqual( | |
oldCodeLines[pb.location.range.lowerBound+1 ..< | |
pb.location.range.upperBound] | |
) | |
{ | |
unchangedPrintBlocks += 1 | |
} | |
newCodeLines.append(contentsOf: printExprResult) | |
lastBlockEnd = pb.location.range.upperBound | |
} | |
if unchangedPrintBlocks == sortedPrintBlocks.count { | |
// If all PrintBlocks unchanged, then there is no need to add | |
// this path to newSwiftFiles. | |
print("All PrintBlocks unhanged: \(path)") | |
} | |
else { | |
// Otherwise (if there are changed PrintBlocks, | |
// add it to newSwiftFiles. | |
// Remember to append the last part: | |
let oldSection = oldCodeLines[lastBlockEnd ..< | |
oldCodeLines.endIndex] | |
newCodeLines.append(contentsOf: oldSection) | |
// Store the new contents for its path: | |
newSwiftFiles[path] = newCodeLines | |
} | |
} | |
return newSwiftFiles | |
}() | |
//----------------------------------------------------------------------------- | |
do { | |
for (path, codeLines) in newSwiftFiles { | |
print("Writing: \(path)") | |
try codeLines.write(toFile: path) | |
} | |
} catch let e { | |
exitWithError(String(describing: e)) | |
} | |
//----------------------------------------------------------------------------- | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//----------------------------------------------------------------------------- | |
// Settings.swift | |
// SwiftInSwift | |
//----------------------------------------------------------------------------- | |
let appName = "SwiftInSwift" | |
let defBlockPrefix = "// DEF-{" | |
let defBlockSuffix = "// }-DEF" | |
let printBlockPrefix = "// PRINT-{" | |
let printBlockSuffix = "// }-PRINT" | |
let tempDirPath = "." | |
let printExpressionEvaluatorScript = ( | |
name: "PrintExpressionEvaluator", | |
shebangLine: "#!/usr/bin/swift", | |
printFuncName: "🖨👉", | |
resultFilename: "PrintExpressionEvaluatorResult" | |
) | |
let bashPath = "/bin/bash" | |
let findPath = "/usr/bin/find" | |
//----------------------------------------------------------------------------- | |
// Check that bash and find are available at bashPath and findPath. | |
//----------------------------------------------------------------------------- | |
let cmdBash = { () -> Executable in | |
guard let cmdBash = Executable(atPath: bashPath) | |
else { exitWithError("error: expected bash at \(bashPath)") } | |
return cmdBash | |
}() | |
let cmdFind = { () -> Executable in | |
guard let cmdFind = Executable(atPath: findPath) | |
else { exitWithError("error: expected find at \(findPath)") } | |
return cmdFind | |
}() | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//----------------------------------------------------------------------------- | |
// StringUtilities.swift | |
// SwiftInSwift | |
//----------------------------------------------------------------------------- | |
import Foundation | |
// Initializing a [String] from a file: | |
extension RangeReplaceableCollection where Iterator.Element == String { | |
init(contentsOfFile path: String, | |
encoding enc: String.Encoding = .utf8) throws | |
{ | |
let s = try String.init(contentsOfFile: path, encoding: enc) | |
self = Self(s.components(separatedBy: "\n")) | |
} | |
} | |
// Writing a [String] to a file. | |
extension Sequence where Iterator.Element == String { | |
func write(toFile path: String, | |
encoding enc: String.Encoding = .utf8) throws | |
{ | |
let s = self.joined(separator: "\n") | |
try s.write(toFile: path, atomically: true, encoding: enc) | |
} | |
} | |
// Some other String extensions. | |
extension String { | |
func removingOneIfAnyTrailingNewLine() -> String { | |
return self.hasSuffix("\n") | |
? String(self.characters.dropLast()) | |
: self | |
} | |
} | |
extension String { | |
func removingPossiblePrefix(_ prefixStr: String) -> String { | |
if self.hasPrefix(prefixStr) == false { return self } | |
return String(self.characters.suffix(from: prefixStr.endIndex)) | |
} | |
func removingRequiredPrefix(_ prefixStr: String) -> String? { | |
if self.hasPrefix(prefixStr) == false { return nil } | |
return String(self.characters.suffix(from: prefixStr.endIndex)) | |
} | |
func removingPossibleSuffix(_ suffixStr: String) -> String { | |
if self.hasSuffix(suffixStr) == false { return self } | |
return String(self.characters | |
.prefix(self.characters.count - suffixStr.characters.count)) | |
} | |
func removingRequiredSuffix(_ suffixStr: String) -> String? { | |
if self.hasSuffix(suffixStr) == false { return nil } | |
return String(self.characters | |
.prefix(self.characters.count - suffixStr.characters.count)) | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//----------------------------------------------------------------------------- | |
// SystemUtilities.swift | |
// SwiftInSwift | |
//----------------------------------------------------------------------------- | |
import Foundation | |
func stdErrPrint(_ string: String, terminator: String = "\n") { | |
struct StdErrOutputStream: TextOutputStream { | |
public mutating func write(_ s: String) { | |
fputs(s.cString(using: .utf8), stderr) | |
} | |
} | |
var errStream = StdErrOutputStream() | |
print(string, terminator: terminator, to: &errStream) | |
} | |
func exitWithError(_ message: String) -> Never { | |
stdErrPrint(message) | |
exit(1) | |
} | |
func exitWithError(path: String, line: Int, message: String) -> Never { | |
stdErrPrint("\(path):\(line): error: \(message)") | |
exit(1) | |
} | |
func exitWithError( | |
path: String, line: Int, char: Int, message: String) -> Never | |
{ | |
stdErrPrint("\(path):\(line):\(char): error: \(message)") | |
exit(1) | |
} | |
struct Executable { | |
struct Result : CustomStringConvertible { | |
let stdOut: String | |
let stdErr: String | |
let exitCode: Int | |
var description: String { | |
return "\nstdOut:\n" + stdOut | |
+ "\n--\nstdErr:\n" + stdErr | |
+ "\n--\nexitCode: \(exitCode)" | |
} | |
var validResult: String? { | |
return stdErr.isEmpty && exitCode == 0 ? stdOut : nil } | |
var validNonEmptyResult: String? { | |
return stdErr.isEmpty && exitCode == 0 && stdOut.isEmpty == false | |
? stdOut | |
: nil } | |
func printResult() { | |
print(validResult ?? "ERROR:\n\(stdErr)\nEXIT-CODE:\n\(exitCode)") | |
} | |
} | |
private let executablePath: String | |
init?(atPath path: String) { | |
if FileManager.default.isExecutableFile(atPath: path) { | |
self.executablePath = path | |
} else { | |
return nil | |
} | |
} | |
func execute(arguments args: [String] = []) -> Result { | |
return execute(currentDirectoryPath: nil, arguments: args) | |
} | |
func execute(currentDirectoryPath path: String?, | |
arguments args: [String] = []) -> Result | |
{ | |
let process = Process() | |
if let workingDir = path { | |
var isDir : ObjCBool = true | |
let isExisting = FileManager.default | |
.fileExists(atPath: workingDir, isDirectory: &isDir) | |
guard isExisting == true && isDir.boolValue == true | |
else { return Result(stdOut: "", | |
stdErr: "currentDirectoryPath: " | |
+ "\(workingDir): No such directory", | |
exitCode: 1) } | |
process.currentDirectoryPath = workingDir | |
} | |
process.launchPath = executablePath | |
process.arguments = args | |
let outPipe = Pipe(); process.standardOutput = outPipe | |
let errPipe = Pipe(); process.standardError = errPipe | |
process.launch() | |
guard | |
let out = String( | |
data: outPipe.fileHandleForReading.readDataToEndOfFile(), | |
encoding: .utf8), | |
let err = String( | |
data: errPipe.fileHandleForReading.readDataToEndOfFile(), | |
encoding: .utf8) | |
else { return Result(stdOut: "", | |
stdErr: "Non-utf8 stdout and/or stderr", | |
exitCode: 1) } | |
process.waitUntilExit() | |
return Result(stdOut: out.removingOneIfAnyTrailingNewLine(), | |
stdErr: err.removingOneIfAnyTrailingNewLine(), | |
exitCode: Int(process.terminationStatus)) | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment