├── .gitignore ├── .swift-version ├── Makefile ├── Package.swift ├── .travis.yml ├── Chores.podspec ├── LICENSE ├── README.md ├── Tests └── Tests.swift └── Sources └── ChoreTask.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .build 2 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 2.2-dev 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | test: 4 | xctester Sources/*.swift Tests/*.swift 5 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | import PackageDescription 2 | 3 | let package = Package( 4 | name: "Chores" 5 | ) 6 | 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode7.2 3 | before_install: 4 | - brew tap neonichu/formulae 5 | - brew install xctester 6 | script: make test 7 | -------------------------------------------------------------------------------- /Chores.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'Chores' 3 | s.version = '0.0.3' 4 | s.summary = 'A library for simplifying task execution in Swift.' 5 | s.homepage = 'https://github.com/neonichu/Chores' 6 | s.license = 'MIT' 7 | 8 | s.author = { 'Boris Bügling' => 'boris@buegling.com' } 9 | s.social_media_url = 'http://twitter.com/NeoNacho' 10 | 11 | s.platform = :osx, '10.9' 12 | s.source = { :git => 'https://github.com/neonichu/Chores.git', 13 | :tag => s.version } 14 | s.source_files = 'Sources' 15 | end 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Boris Bügling 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chores 2 | 3 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 4 | [![Version](https://img.shields.io/cocoapods/v/Chores.svg?style=flat)](http://cocoadocs.org/docsets/Chores) 5 | [![License](https://img.shields.io/cocoapods/l/Chores.svg?style=flat)](http://cocoadocs.org/docsets/Chores) 6 | [![Platform](https://img.shields.io/cocoapods/p/Chores.svg?style=flat)](http://cocoadocs.org/docsets/Chores) 7 | [![Build Status](http://img.shields.io/travis/neonichu/Chores.svg?style=flat)](https://travis-ci.org/neonichu/Chores) 8 | 9 | A library for simplifying task execution in Swift. 10 | 11 | ## Usage 12 | 13 | Use the `>` custom operator to execute commands: 14 | 15 | ```swift 16 | let result = >"true" 17 | print(result.result) // 0 18 | print(result.stdout) // "" 19 | ``` 20 | 21 | You can also create pipes using '|' custom operator: 22 | 23 | ```swift 24 | let result = >"ls"|["grep", ".md$"] 25 | print(result.stdout) // "README.md" 26 | ``` 27 | 28 | And pipe commands into a closure: 29 | 30 | ```swift 31 | let result = >["ls", "README.md"]|{ String(count($0)) } 32 | print(result.stdout) // "9" 33 | ``` 34 | 35 | ## Unit Tests 36 | 37 | The tests require [xctester][1], install it via [Homebrew][2]: 38 | 39 | ``` 40 | $ brew tap neonichu/formulae 41 | $ brew install xctester 42 | ``` 43 | 44 | and run the tests: 45 | 46 | ``` 47 | $ make test 48 | ``` 49 | 50 | [1]: https://github.com/neonichu/xctester 51 | [2]: http://brew.sh 52 | 53 | -------------------------------------------------------------------------------- /Tests/Tests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | class ChoreTests : XCTestCase { 4 | func testStandardOutput() { 5 | let result = >["/bin/echo", "#yolo"] 6 | 7 | XCTAssertEqual(result.result, 0) 8 | XCTAssertEqual(result.stdout, "#yolo") 9 | XCTAssertEqual(result.stderr, "") 10 | } 11 | 12 | func testStandardError() { 13 | let result = >["/bin/sh", "-c", "echo yolo >&2"] 14 | 15 | XCTAssertEqual(result.result, 0) 16 | XCTAssertEqual(result.stdout, "") 17 | XCTAssertEqual(result.stderr, "yolo") 18 | } 19 | 20 | func testResult() { 21 | let result = >["/usr/bin/false"] 22 | 23 | XCTAssertEqual(result.result, 1) 24 | } 25 | 26 | func testResolvesCommandPathsIfNotAbsolute() { 27 | let result = >"true" 28 | 29 | XCTAssertEqual(result.result, 0) 30 | } 31 | 32 | func testFailsWithNonExistingCommand() { 33 | let result = >"/bin/yolo" 34 | 35 | XCTAssertEqual(result.result, 255) 36 | XCTAssertEqual(result.stdout, "") 37 | XCTAssertEqual(result.stderr, "/bin/yolo: launch path not accessible") 38 | } 39 | 40 | func testFailsToExecuteDirectory() { 41 | let result = >"/" 42 | 43 | XCTAssertEqual(result.result, 255) 44 | XCTAssertEqual(result.stdout, "") 45 | XCTAssertEqual(result.stderr, "/: launch path is a directory") 46 | } 47 | 48 | func testFailsToExecuteNonExecutableFile() { 49 | let result = >"/etc/passwd" 50 | 51 | XCTAssertEqual(result.result, 255) 52 | XCTAssertEqual(result.stdout, "") 53 | XCTAssertEqual(result.stderr, "/etc/passwd: launch path not executable") 54 | } 55 | 56 | func testSimplePipe() { 57 | let result = >"ls"|"cat" 58 | 59 | XCTAssertEqual(result.result, 0) 60 | XCTAssertTrue(result.stdout.characters.count > 0) 61 | XCTAssertEqual(result.stderr, "") 62 | } 63 | 64 | func testPipeWithArguments() { 65 | let result = >["ls", "README.md"]|["sed", "s/READ/EAT/"] 66 | 67 | XCTAssertEqual(result.result, 0) 68 | XCTAssertEqual(result.stdout, "EATME.md") 69 | XCTAssertEqual(result.stderr, "") 70 | } 71 | 72 | func testPipeFail() { 73 | let result = >["ls", "yolo"]|"cat" 74 | 75 | XCTAssertEqual(result.result, 1) 76 | XCTAssertEqual(result.stdout, "") 77 | XCTAssertEqual(result.stderr, "ls: yolo: No such file or directory") 78 | } 79 | 80 | func testPipeToClosure() { 81 | let result = >["ls", "LICENSE"]|{ String($0.characters.count) } 82 | 83 | XCTAssertEqual(result.stdout, "7") 84 | } 85 | 86 | func testPipeToClosureFail() { 87 | let result = >["ls", "yolo"]|{ String($0.characters.count) } 88 | 89 | XCTAssertEqual(result.result, 1) 90 | XCTAssertEqual(result.stdout, "") 91 | XCTAssertEqual(result.stderr, "ls: yolo: No such file or directory") 92 | } 93 | 94 | func testPipeClosureIntoCommand() { 95 | let result = { "yolo" }|"cat" 96 | 97 | XCTAssertEqual(result.result, 0) 98 | XCTAssertEqual(result.stdout, "yolo") 99 | XCTAssertEqual(result.stderr, "") 100 | } 101 | 102 | func testPipeStringIntoCommand() { 103 | let result = "yolo"|"cat" 104 | 105 | XCTAssertEqual(result.result, 0) 106 | XCTAssertEqual(result.stdout, "yolo") 107 | XCTAssertEqual(result.stderr, "") 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Sources/ChoreTask.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// The result of a task execution 4 | public typealias ChoreResult = (result: Int32, stdout: String, stderr: String) 5 | 6 | private func string_trim(string: NSString!) -> String { 7 | return string.stringByTrimmingCharactersInSet(.whitespaceAndNewlineCharacterSet()) ?? "" 8 | } 9 | 10 | private func chore_task(command: String, _ arguments: [String] = [String](), stdin: String = "") -> ChoreResult { 11 | let task = NSTask() 12 | 13 | task.launchPath = command 14 | task.arguments = arguments 15 | 16 | if !(task.launchPath! as NSString).absolutePath { 17 | task.launchPath = (chore_task("/usr/bin/which", [task.launchPath!])).stdout 18 | } 19 | 20 | var isDirectory: ObjCBool = false 21 | 22 | if !NSFileManager.defaultManager().fileExistsAtPath(task.launchPath!, isDirectory: &isDirectory) { 23 | return (255, "", String(format: "%@: launch path not accessible", task.launchPath!)) 24 | } 25 | 26 | if (isDirectory) { 27 | return (255, "", String(format: "%@: launch path is a directory", task.launchPath!)) 28 | } 29 | 30 | if !NSFileManager.defaultManager().isExecutableFileAtPath(task.launchPath!) { 31 | return (255, "", String(format: "%@: launch path not executable", task.launchPath!)) 32 | } 33 | 34 | if stdin.characters.count > 0 { 35 | let stdinPipe = NSPipe() 36 | task.standardInput = stdinPipe 37 | let stdinHandle = stdinPipe.fileHandleForWriting 38 | 39 | if let data = stdin.dataUsingEncoding(NSUTF8StringEncoding) { 40 | stdinHandle.writeData(data) 41 | stdinHandle.closeFile() 42 | } 43 | } 44 | 45 | let stderrPipe = NSPipe() 46 | task.standardError = stderrPipe 47 | let stderrHandle = stderrPipe.fileHandleForReading 48 | 49 | let stdoutPipe = NSPipe() 50 | task.standardOutput = stdoutPipe 51 | let stdoutHandle = stdoutPipe.fileHandleForReading 52 | 53 | task.launch() 54 | task.waitUntilExit() 55 | 56 | let stderr = string_trim(NSString(data: stderrHandle.readDataToEndOfFile(), encoding: NSUTF8StringEncoding)) ?? "" 57 | let stdout = string_trim(NSString(data: stdoutHandle.readDataToEndOfFile(), encoding: NSUTF8StringEncoding)) ?? "" 58 | 59 | return (task.terminationStatus, stdout, stderr) 60 | } 61 | 62 | prefix operator > {} 63 | 64 | /** 65 | Executes a command. 66 | 67 | :param: command The command to execute. 68 | :returns: A tuple containing the exit code, stdout and stderr output. 69 | */ 70 | public prefix func > (command: String) -> ChoreResult { 71 | return chore_task(command) 72 | } 73 | 74 | /** 75 | Executes a command with arguments. 76 | 77 | :param: command The command to execute and its arguments. 78 | :returns: A tuple containing the exit code, stdout and stderr output. 79 | */ 80 | public prefix func > (command: [String]) -> ChoreResult { 81 | switch command.count { 82 | case 0: 83 | return (0, "", "") 84 | case 1: 85 | return chore_task(command[0]) 86 | default: 87 | break 88 | } 89 | 90 | return chore_task(command[0], Array(command[1.. ChoreResult { 103 | return left|[right] 104 | } 105 | 106 | /** 107 | Executes a command with standard input from another command. 108 | 109 | :param: left The result of a previous command. 110 | :param: right The command to execute and its arguments. 111 | :returns: A tuple containing the exit code, stdout and stderr output. 112 | */ 113 | public func | (left: ChoreResult, right: [String]) -> ChoreResult { 114 | if left.result != 0 { 115 | return left 116 | } 117 | 118 | let arguments = right.count >= 2 ? Array(right[1.. String)) -> ChoreResult { 130 | if left.result != 0 { 131 | return left 132 | } 133 | 134 | return (0, right(left.stdout), "") 135 | } 136 | 137 | /** 138 | Executes a command with input from a closure. 139 | 140 | :param: left The closure to execute. 141 | :param: right The command to execute. 142 | :returns: A tuple containing the exit code, stdout and stderr output. 143 | */ 144 | public func | (left: (() -> String), right: String) -> ChoreResult { 145 | return (0, left(), "")|right 146 | } 147 | 148 | /** 149 | Executes a command with input from a closure. 150 | 151 | :param: left The closure to execute. 152 | :param: right The command to execute and its arguments. 153 | :returns: A tuple containing the exit code, stdout and stderr output. 154 | */ 155 | public func | (left: (() -> String), right: [String]) -> ChoreResult { 156 | return (0, left(), "")|right 157 | } 158 | 159 | /** 160 | Executes a command with input from a string. 161 | 162 | :param: left The string to use a stdin. 163 | :param: right The command to execute. 164 | :returns: A tuple containing the exit code, stdout and stderr output. 165 | */ 166 | public func | (left: String, right: String) -> ChoreResult { 167 | return (0, left, "")|right 168 | } 169 | 170 | /** 171 | Executes a command with input from a string. 172 | 173 | :param: left The string to use a stdin. 174 | :param: right The command to execute and its arguments. 175 | :returns: A tuple containing the exit code, stdout and stderr output. 176 | */ 177 | public func | (left: String, right: [String]) -> ChoreResult { 178 | return (0, left, "")|right 179 | } 180 | --------------------------------------------------------------------------------