Skip to content

Instantly share code, notes, and snippets.

@griotspeak
Forked from anonymous/Settings.swift
Created September 21, 2016 01:31
Show Gist options
  • Save griotspeak/b23670aaa422581226877e3fdc147864 to your computer and use it in GitHub Desktop.
Save griotspeak/b23670aaa422581226877e3fdc147864 to your computer and use it in GitHub Desktop.
SwiftInSwift - A poor man's meta programming tool for Swift
//-----------------------------------------------------------------------------
// 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))
}
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
// 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
}()
//-----------------------------------------------------------------------------
// 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))
}
}
//-----------------------------------------------------------------------------
// 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