From e45515ec889e91bb0a3d2fdfdc1557bbf05b7a74 Mon Sep 17 00:00:00 2001 From: David Owens II Date: Mon, 6 Jul 2015 00:09:37 -0700 Subject: [PATCH] Refactored the code to be ready for the 0.1.0 release. --- LICENSE | 21 +++++ README.md | 66 +++++++++++++++- apous.xcodeproj/project.pbxproj | 16 ++++ src/ErrorCodes.swift | 14 ++++ src/Tools.swift | 134 ++++++++++++++++++++++++++++++++ src/Utils.swift | 63 +++++++++++++++ src/main.swift | 124 ++++++++++++----------------- 7 files changed, 359 insertions(+), 79 deletions(-) create mode 100644 LICENSE create mode 100644 src/ErrorCodes.swift create mode 100644 src/Tools.swift create mode 100644 src/Utils.swift diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2ad8c0f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Kiad Software, LLC. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index ce19ea4..464067e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,65 @@ -# apous -Let's make Swift scripting a reality. + _______ + | _ .-----.-----.--.--.-----. + |. 1 | _ | _ | | |__ --| + |. _ | __|_____|_____|_____| + |: | |__| + |::.|:. | + `--- ---' +Apous is a simple tool that allows for easier authoring of Swift scripts. -# Workflow +Primary features: -> apous init + 1. Allow the breaking up of scripts into multiple files. + 2. Dependency management through [Carthage](https://github.com/Carthage/Carthage). + +# How it Works + +Apous works by first checking for a `Cartfile` in your script's directory. If one is +present, then `carthage update` will be run. + +Next, all of your Swift files are combined into a single `.apous.swift` file that can +then be run by the `swift` REPL. + +It's really that simple. + +# Getting Started + +First, you need to install the latest build of Apous. + +TODO(owensd): Finish the install steps. + + +# Creating Your First Script + +1. Create a new directory for your scripts, say `mkdir demo` +2. Change to that directory: `cd demo` +3. Create a new script file: `touch demo.swift` +4. Change the contents of the file to: + +```swift +import Foundation + +print("Welcome to Apous!") +``` + +5. Run the script: `apous demo.swift` + +This will output: + + Welcome to Apous! + +You can see some other samples here: [Samples](https://github.com/owensd/apous/tree/master/samples). + + +# Known Issues + +Currently there are some design limitations: + + * [Issue #1](https://github.com/owensd/apous/issues/1) - Support for nested directories. + * [Issue #2](https://github.com/owensd/apous/issues/2) - Support for folder structure packages. + +# FAQ + +Q: What is Apous mean? +A: It's from the ancient Greek απους, meaning "without feet". diff --git a/apous.xcodeproj/project.pbxproj b/apous.xcodeproj/project.pbxproj index e580277..aca5ae5 100644 --- a/apous.xcodeproj/project.pbxproj +++ b/apous.xcodeproj/project.pbxproj @@ -9,6 +9,9 @@ /* Begin PBXBuildFile section */ 7D0373391B49021700E2711D /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0373381B49021700E2711D /* main.swift */; }; 7D0373401B49028E00E2711D /* apous in Deploy Locally */ = {isa = PBXBuildFile; fileRef = 7D0373351B49021700E2711D /* apous */; }; + 7D3AC3461B49F99B0068CC83 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D3AC3451B49F99B0068CC83 /* Utils.swift */; }; + 7D3AC3481B49FE170068CC83 /* ErrorCodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D3AC3471B49FE170068CC83 /* ErrorCodes.swift */; }; + 7D3AC34A1B4A37BC0068CC83 /* Tools.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D3AC3491B4A37BC0068CC83 /* Tools.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -37,6 +40,11 @@ /* Begin PBXFileReference section */ 7D0373351B49021700E2711D /* apous */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = apous; sourceTree = BUILT_PRODUCTS_DIR; }; 7D0373381B49021700E2711D /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + 7D3AC3421B49F1FC0068CC83 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 7D3AC3441B49F62F0068CC83 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; + 7D3AC3451B49F99B0068CC83 /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; + 7D3AC3471B49FE170068CC83 /* ErrorCodes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorCodes.swift; sourceTree = ""; }; + 7D3AC3491B4A37BC0068CC83 /* Tools.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tools.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -53,6 +61,8 @@ 7D03732C1B49021700E2711D = { isa = PBXGroup; children = ( + 7D3AC3441B49F62F0068CC83 /* LICENSE */, + 7D3AC3421B49F1FC0068CC83 /* README.md */, 7D0373371B49021700E2711D /* src */, 7D0373361B49021700E2711D /* Products */, ); @@ -70,6 +80,9 @@ isa = PBXGroup; children = ( 7D0373381B49021700E2711D /* main.swift */, + 7D3AC3451B49F99B0068CC83 /* Utils.swift */, + 7D3AC3471B49FE170068CC83 /* ErrorCodes.swift */, + 7D3AC3491B4A37BC0068CC83 /* Tools.swift */, ); path = src; sourceTree = ""; @@ -131,7 +144,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7D3AC3461B49F99B0068CC83 /* Utils.swift in Sources */, + 7D3AC3481B49FE170068CC83 /* ErrorCodes.swift in Sources */, 7D0373391B49021700E2711D /* main.swift in Sources */, + 7D3AC34A1B4A37BC0068CC83 /* Tools.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/src/ErrorCodes.swift b/src/ErrorCodes.swift new file mode 100644 index 0000000..418d8f1 --- /dev/null +++ b/src/ErrorCodes.swift @@ -0,0 +1,14 @@ +// +// ErrorCodes.swift +// apous +// +// Created by David Owens on 7/5/15. +// Copyright © 2015 owensd.io. All rights reserved. +// + +enum ErrorCode: Int, ErrorType { + case InvalidUsage = 1 + case PathNotFound + case CarthageNotInstalled + case SwiftNotInstalled +} diff --git a/src/Tools.swift b/src/Tools.swift new file mode 100644 index 0000000..c85fb91 --- /dev/null +++ b/src/Tools.swift @@ -0,0 +1,134 @@ +// +// Tools.swift +// apous +// +// Created by David Owens on 7/5/15. +// Copyright © 2015 owensd.io. All rights reserved. +// + +import Foundation + +/// The base abstraction for all command-line utilities. +protocol Tool { + + /// The name of the tool to be run. + var launchPath: String { get } + + /// `true` when the output of the tool should be printed to `stdout`. + var printOutput: Bool { get } + + /// Runs the tool and returns result of the execution. + func run(args: String...) -> (out: String, err: String, code: Int32) +} + +extension Tool { + var printOutput: Bool { return true } + + func run(args: String...) -> (out: String, err: String, code: Int32) { + let output = NSPipe() + let error = NSPipe() + + // NOTE(owensd): This merges stdout and stderr for now... + func stream(handle: NSFileHandle) -> String { + let data = handle.availableData + let str = NSString(data: data, encoding: NSUTF8StringEncoding) ?? "" + + if self.printOutput { + print("\(str)", appendNewline: false) + } + + return str as String + } + + var out = "" + var err = "" + + output.fileHandleForReading.readabilityHandler = { out += stream($0) } + error.fileHandleForReading.readabilityHandler = { err += stream($0) } + + let task = NSTask() + task.launchPath = self.launchPath + task.arguments = args + task.standardOutput = output + task.standardError = error + task.terminationHandler = { + ($0.standardOutput as? NSFileHandle)?.readabilityHandler = nil + ($0.standardError as? NSFileHandle)?.readabilityHandler = nil + } + + task.launch() + task.waitUntilExit() + + return ( + out: out.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet()), + err: err.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet()), + code: task.terminationStatus) + } + +} + + +struct WhichTool : Tool { + let printOutput = false + let launchPath = "/usr/bin/which" +} + +struct CarthageTool : Tool { + let launchPath: String + + init?() { + let which = WhichTool() + let result = which.run("carthage") + if result.out.characters.count == 0 { return nil } + self.launchPath = result.out + } + + // HACK(owensd): I cannot figure out why this tool will not flush our to stdout in real-time, + // so forcing it to write to stdout for now. + func run(args: String...) -> (out: String, err: String, code: Int32) { + let output = NSFileHandle.fileHandleWithStandardOutput() + let error = NSFileHandle.fileHandleWithStandardError() + + var out = "" + var err = "" + + func stream(handle: NSFileHandle) -> String { + let data = handle.availableData + let str = NSString(data: data, encoding: NSUTF8StringEncoding) ?? "" + return str as String + } + + // NOTE(owensd): These don't work for stdout and stderr... + output.readabilityHandler = { out += stream($0) } + error.readabilityHandler = { err += stream($0) } + + let task = NSTask() + task.launchPath = self.launchPath + task.arguments = args + task.standardOutput = output + task.standardError = error + task.terminationHandler = { + ($0.standardOutput as? NSFileHandle)?.readabilityHandler = nil + ($0.standardError as? NSFileHandle)?.readabilityHandler = nil + } + + task.launch() + task.waitUntilExit() + + return ( + out: out.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet()), + err: err.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet()), + code: task.terminationStatus) + } +} + +struct SwiftTool : Tool { + let launchPath: String + + init?() { + let which = WhichTool() + let result = which.run("swift") + if result.out.characters.count == 0 { return nil } + self.launchPath = result.out + } +} diff --git a/src/Utils.swift b/src/Utils.swift new file mode 100644 index 0000000..e31b16b --- /dev/null +++ b/src/Utils.swift @@ -0,0 +1,63 @@ +// +// Utils.swift +// apous +// +// Created by David Owens on 7/5/15. +// Copyright © 2015 owensd.io. All rights reserved. +// + +import Foundation + +/// Returns the root path that contains the script(s). +/// This is +func canonicalPath(item: String) throws -> String { + func extract() -> String { + if item.pathExtension == "swift" { + let path = item.stringByDeletingLastPathComponent + if path == "" { + return NSFileManager.defaultManager().currentDirectoryPath + } + + return path + } + else { + return item + } + } + + let path = extract().stringByStandardizingPath + + var isDirectory: ObjCBool = false + if NSFileManager.defaultManager().fileExistsAtPath(path, isDirectory: &isDirectory) { + return path + } + + throw ErrorCode.PathNotFound +} + +/// Exit the process error with the given `ErrorCode`. +@noreturn func exit(code: ErrorCode) { + exit(Int32(code.rawValue)) +} + +/// Returns the full path of the valid script files at the given `path`. +func filesAtPath(path: String) -> [String] { + if DebugOutputEnabled { + print("[debug] Finding script files at path: \(path)") + } + + let items: [String] = { + do { + return try NSFileManager.defaultManager().contentsOfDirectoryAtPath(path) + } + catch { + return [] + } + }() + + return items + .filter() { $0 != ".apous.swift" && $0.pathExtension == "swift" } + .map() { path.stringByAppendingPathComponent($0) } +} + + diff --git a/src/main.swift b/src/main.swift index 51d8e5a..8ab56bd 100644 --- a/src/main.swift +++ b/src/main.swift @@ -8,100 +8,74 @@ import Foundation +let CartfileConfig = "Cartfile" +let ApousScriptFile = ".apous.swift" -// 1. Check for Cartfile, build deps -// 2. Check for Portfile, build deps -// 3. Concat all .swift files into .apous.swift -// 4. swiftc .apous.swift - -enum ErrorCodes: Int, ErrorType { - case InvalidUsage = 1 - case PathNotFound -} +// TODO(owensd): Pull this from a proper versioning tool. +let Version = "0.1.0" +let Branch = "master" func printUsage() { - print("that's cute... maybe one day") -} - -func exit(code: ErrorCodes) { - exit(Int32(code.rawValue)) + print("OVERVIEW: Apous Swift Script Runner (build: \(Version)-\(Branch))") + print("") + print("USAGE: apous [|]") } -func path(item: String) throws -> String { - func extract() -> String { - if item.pathExtension == "swift" { - print("handle swift file") - return item.stringByDeletingLastPathComponent - } - else { - return item - } - } - - let path = extract().stringByStandardizingPath - - var isDirectory: ObjCBool = false - if NSFileManager.defaultManager().fileExistsAtPath(path, isDirectory: &isDirectory) { - print("isDirectory: \(isDirectory)") - return path - } +// +// The body of the script. +// - throw ErrorCodes.PathNotFound -} +let arguments = NSProcessInfo.processInfo().arguments -func filesAtPath(path: String) -> [String] { - let items: [String] = { - do { - return try NSFileManager.defaultManager().contentsOfDirectoryAtPath(path) - } - catch { - return [] - } - }() - - return items - .filter() { $0 != ".apous.swift" } - .filter() { $0.pathExtension == "swift" } - .map() { path.stringByAppendingPathComponent($0) } +if arguments.contains("-help") { + printUsage() + exit(0) } -func runTask(task: String, _ args: String...) { - - let t = NSTask() - t.launchPath = task - t.arguments = args - t.standardOutput = NSFileHandle.fileHandleWithStandardOutput() - t.standardError = NSFileHandle.fileHandleWithStandardError() - t.launch() - t.waitUntilExit() -} +// This is used to enable more verbose logging. +let DebugOutputEnabled = arguments.contains("-debug") +// NOTE(owensd): This method is a workaround because of Swift bugs and code in the top-level scope. +func run() throws { + let scriptItem = arguments[1..