├── .github ├── CODEOWNERS ├── FUNDING.yml └── workflows │ └── swift.yml ├── .gitignore ├── .swiftlint.yml ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── Package.swift ├── README.md ├── Sources └── ProcessRunner │ ├── ProcessResult.swift │ ├── ProcessRunner.swift │ └── Sugar.swift ├── Tests ├── LinuxMain.swift └── ProcessRunnerTests │ ├── CurrentDirectoryTests.swift │ ├── ProcessRunnerTests.swift │ ├── StandardErrorTests.swift │ ├── StandardOutputTests.swift │ ├── SuccessFailureTests.swift │ └── XCTestManifests.swift ├── codecov.yml ├── docs ├── Package.md ├── PackageModules.dot ├── PackageModules.png ├── README.md ├── classes │ └── ProcessRunner.md ├── methods │ ├── system(command_captureOutput_currentDirectoryPath_).md │ ├── system(command_parameters_captureOutput_currentDirectoryPath_).md │ └── system(shell_captureOutput_currentDirectoryPath_).md └── structs │ └── ProcessResult.md └── processrunner.png /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @eneko 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: eneko 4 | patreon: eneko 5 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | lint: 7 | name: SwiftLint 8 | runs-on: macOS-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Install 12 | run: brew install swiftlint 13 | - name: Lint 14 | run: make lint 15 | 16 | build: 17 | name: MacOS Latest 18 | runs-on: macOS-latest 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Build 22 | run: swift build 23 | - name: Run tests 24 | run: swift test 25 | - name: Run tests in parallel 26 | run: swift test --parallel 27 | 28 | ubuntu: 29 | name: Ubuntu Latest 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v2 33 | - name: Build 34 | run: swift build 35 | - name: Run tests 36 | run: swift test 37 | - name: Run tests in parallel 38 | run: swift test --parallel 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | cat 6 | 7 | *.xcodeproj 8 | 9 | ## Build generated 10 | build/ 11 | DerivedData/ 12 | 13 | ## Various settings 14 | *.pbxuser 15 | !default.pbxuser 16 | *.mode1v3 17 | !default.mode1v3 18 | *.mode2v3 19 | !default.mode2v3 20 | *.perspectivev3 21 | !default.perspectivev3 22 | xcuserdata/ 23 | 24 | ## Other 25 | *.moved-aside 26 | *.xccheckout 27 | *.xcscmblueprint 28 | 29 | ## Obj-C/Swift specific 30 | *.hmap 31 | *.ipa 32 | *.dSYM.zip 33 | *.dSYM 34 | 35 | ## Playgrounds 36 | timeline.xctimeline 37 | playground.xcworkspace 38 | 39 | # Swift Package Manager 40 | # 41 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 42 | # Packages/ 43 | # Package.pins 44 | # Package.resolved 45 | .build/ 46 | .swiftpm/ 47 | 48 | # CocoaPods 49 | # 50 | # We recommend against adding the Pods directory to your .gitignore. However 51 | # you should judge for yourself, the pros and cons are mentioned at: 52 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 53 | # 54 | # Pods/ 55 | 56 | # Carthage 57 | # 58 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 59 | # Carthage/Checkouts 60 | 61 | Carthage/Build 62 | 63 | # fastlane 64 | # 65 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 66 | # screenshots whenever they are needed. 67 | # For more information about the recommended setup visit: 68 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 69 | 70 | fastlane/report.xml 71 | fastlane/Preview.html 72 | fastlane/screenshots/**/*.png 73 | fastlane/test_output 74 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | excluded: 2 | - .build 3 | - Tests/LinuxMain.swift 4 | - Tests/ProcessRunnerTests/XCTestManifests.swift 5 | 6 | 7 | opt_in_rules: 8 | - attributes 9 | - closure_end_indentation 10 | - closure_spacing 11 | - conditional_returns_on_newline 12 | - empty_count 13 | - fatal_error_message 14 | - first_where 15 | - force_unwrapping 16 | - joined_default_parameter 17 | - nimble_operator 18 | - no_extension_access_modifier 19 | - number_separator 20 | - object_literal 21 | - operator_usage_whitespace 22 | - overridden_super_call 23 | - pattern_matching_keywords 24 | - private_outlet 25 | - prohibited_super_call 26 | - quick_discouraged_call 27 | - redundant_nil_coalescing 28 | - single_test_class 29 | - switch_case_on_newline 30 | - trailing_closure 31 | - unneeded_parentheses_in_closure_argument 32 | - vertical_parameter_alignment_on_call 33 | 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: generic 2 | 3 | matrix: 4 | include: 5 | - os: linux 6 | name: Linux Trusty 7 | dist: trusty 8 | sudo: required 9 | - os: osx 10 | name: macOS 11 | osx_image: xcode10.3 12 | 13 | install: 14 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then 15 | eval "$(curl -sL https://swiftenv.fuller.li/install.sh)"; 16 | swiftenv install 5.0; 17 | swiftenv local 5.0; 18 | fi 19 | - swift --version 20 | 21 | script: 22 | - swift test 23 | - swift test --parallel 24 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then 25 | swift package generate-xcodeproj --enable-code-coverage; 26 | xcodebuild -scheme ProcessRunner-Package test; 27 | fi 28 | 29 | after_success: 30 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then 31 | bash <(curl -s https://codecov.io/bash); 32 | fi 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | #FROM swiftlang/swift:nightly-master- 2 | #FROM swift:amazonlinux2 3 | #FROM swift:ubuntu-latest 4 | FROM swift:latest 5 | 6 | #RUN yum -y install git zip 7 | 8 | WORKDIR /tmp 9 | 10 | ADD Sources ./Sources 11 | ADD Tests ./Tests 12 | ADD Package.swift ./ 13 | 14 | CMD swift test --enable-test-discovery 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Eneko Alonso 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: docs 2 | 3 | docs: 4 | sourcedocs generate --spm-module System --output-folder docs --clean 5 | sourcedocs package -o docs 6 | 7 | lint: 8 | swiftlint autocorrect --format --quiet 9 | swiftlint lint --quiet --strict 10 | 11 | dockertest: 12 | docker build -f Dockerfile -t linuxtest . 13 | docker run linuxtest 14 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "ProcessRunner", 6 | platforms: [ 7 | .macOS(.v10_13) 8 | ], 9 | products: [ 10 | .library(name: "ProcessRunner", targets: ["ProcessRunner"]) 11 | ], 12 | targets: [ 13 | .target(name: "ProcessRunner", dependencies: []), 14 | .testTarget(name: "ProcessRunnerTests", dependencies: ["ProcessRunner"]) 15 | ] 16 | ) 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ProcessRunner 2 | 3 | ![ProcessRunner](/processrunner.png) 4 | 5 | ![Release](https://img.shields.io/github/release/eneko/System.svg) 6 | ![Swift 5.0](https://img.shields.io/badge/Swift-5.0-orange.svg) 7 | [![Build Status](https://travis-ci.org/eneko/System.svg?branch=master)](https://travis-ci.org/eneko/System) 8 | [![codecov](https://codecov.io/gh/eneko/System/branch/master/graph/badge.svg)](https://codecov.io/gh/eneko/System) 9 | [![codebeat badge](https://codebeat.co/badges/51605ed0-b4dc-498f-9b45-375ef5011659)](https://codebeat.co/projects/github-com-eneko-system-master) 10 | ![Linux Compatible](https://img.shields.io/badge/linux-compatible%20🐧-brightgreen.svg) 11 | 12 | Easily execute system commands from a Swift script or command line tool. 13 | 14 | **Features** 15 | - ✅ Easily execute child processes with arguments 16 | - ✅ Easily execute shell commands with arguments 17 | - ✅ Capture output or stream to stdout/stderr in real time 18 | - ✅ Swift Package Manager compatible 19 | - ✅ Linux compatible 🐧 20 | 21 | 22 | ## 🚀 Executing child Processes from Swift scripts and CLI tools 23 | Running child processes in Swift is not hard with `Process`, but it can be a 24 | bit tedious and repetitive. 25 | 26 | `System` makes this task extremely easy. If you are familiar with Ruby 27 | scripting (Rakefile, Fastlane, Danger, etc), you will feel like home. 28 | 29 | ### 💻 Automatically redirect output to stdout 30 | 31 | ```swift 32 | import ProcessRunner 33 | 34 | try system(command: "echo hello world") // prints "hello world" to stdout 35 | ``` 36 | 37 | ### ✇ Capture process output 38 | 39 | ```swift 40 | import ProcessRunner 41 | 42 | let output = try system(command: "echo hello world", captureOutput: true).standardOutput 43 | print(output) // prints "hello world" 44 | ``` 45 | 46 | ### ✔️ Check if process terminated gracefully 47 | 48 | ```swift 49 | import ProcessRunner 50 | 51 | print(try system(command: "echo hello world").success) // prints "true" 52 | ``` 53 | 54 | ### |> Easily execute Shell commands with pipes and redirects 55 | 56 | ```swift 57 | import ProcessRunner 58 | 59 | try system(shell: "echo hello cat > cat && cat cat | awk '{print $2}'") // prints "cat" to stdout 60 | ``` 61 | 62 | ## Installation 63 | 64 | Add `ProcessRunner` to your `Package.swift`: 65 | 66 | ```swift 67 | import PackageDescription 68 | 69 | let package = Package( 70 | name: "YourPackage", 71 | dependencies: [ 72 | .package(url: "git@github.com:eneko/ProcessRunner.git", from: "1.0.0"), 73 | ], 74 | targets: [ 75 | .target( 76 | name: "YourTarget", 77 | dependencies: ["ProcessRunner"]), 78 | ] 79 | ) 80 | ``` 81 | 82 | 83 | ## 💌 Contact 84 | Follow and/or contact me on Twitter at [@eneko](https://www.twitter.com/eneko). 85 | 86 | ## 👏 Contributions 87 | If you find an issue, just [open a ticket](https://github.com/eneko/System/issues/new) 88 | on it. Pull requests are warmly welcome as well. 89 | 90 | ## 👮‍♂️ License 91 | System is licensed under the MIT license. See [LICENSE](/LICENSE) for more info. 92 | -------------------------------------------------------------------------------- /Sources/ProcessRunner/ProcessResult.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Process result, including captured output if required 4 | public struct ProcessResult { 5 | /// Process exit status code 6 | public let exitStatus: Int 7 | 8 | /// Standard output, if captured 9 | public let standardOutput: String 10 | 11 | /// Standard error output, if captured 12 | public let standardError: String 13 | 14 | /// Returns `true` if the process finalized with exit code 0. 15 | public var success: Bool { 16 | return exitStatus == 0 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/ProcessRunner/ProcessRunner.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Process runner to execute system commands 4 | public class ProcessRunner { 5 | public let command: String 6 | public let arguments: [String] 7 | public let captureOutput: Bool 8 | public let currentDirectoryPath: String? 9 | 10 | /// Initialize process runner 11 | /// - Parameters: 12 | /// - command: Absolute path to the binary executable 13 | /// - arguments: List of arguments to pass to the executable 14 | /// - captureOutput: Capture stdout and stderr 15 | /// - currentDirectoryPath: Specify current directory for child process (optional) 16 | public init(command: String, arguments: [String], captureOutput: Bool, currentDirectoryPath: String? = nil) { 17 | self.command = command 18 | self.arguments = arguments 19 | self.captureOutput = captureOutput 20 | self.currentDirectoryPath = currentDirectoryPath 21 | } 22 | 23 | /// Execute the process 24 | /// - Throws: Throws error if process fails to run 25 | /// - Returns: Process execution result, including status code and captured output 26 | public func run() throws -> ProcessResult { 27 | let process = Process() 28 | process.executableURL = URL(fileURLWithPath: command) 29 | process.arguments = arguments 30 | 31 | if let path = currentDirectoryPath { 32 | process.currentDirectoryURL = URL(fileURLWithPath: path) 33 | } 34 | 35 | let outputPipe = Pipe() 36 | let errorPipe = Pipe() 37 | if captureOutput { 38 | process.standardOutput = outputPipe 39 | process.standardError = errorPipe 40 | } 41 | 42 | try process.run() 43 | process.waitUntilExit() 44 | 45 | let standardOutput = captureOutput ? outputPipe.string ?? "" : "" 46 | let standardError = captureOutput ? errorPipe.string ?? "" : "" 47 | return ProcessResult(exitStatus: Int(process.terminationStatus), 48 | standardOutput: standardOutput, standardError: standardError) 49 | } 50 | } 51 | 52 | extension Pipe { 53 | var string: String? { 54 | let data = fileHandleForReading.readDataToEndOfFile() 55 | return String(data: data, encoding: .utf8)? 56 | .trimmingCharacters(in: .whitespacesAndNewlines) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/ProcessRunner/Sugar.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Executes command with parameters as a child process 4 | /// 5 | /// `system` will wait for the process to finish, blocking the current thread. 6 | /// 7 | /// - Parameters: 8 | /// - command: Command to execute (full binary path or name of executable in $PATH). 9 | /// - parameters: List of parameters to pass to the command. 10 | /// - captureOutput: If output is captured, both stdout and strerr will be available in 11 | /// the return object. Otherwise, process output will be forwarded to stdout and stderr. 12 | /// Defaults to `false`. 13 | /// - currentDirectoryPath: Specify current directory for child process (optional) 14 | /// - Returns: Process result data which is available after process termination. 15 | /// The `ProcessResult` object includes exit code or termination signal and any captured output. 16 | /// - Throws: `SystemError.waitpid` if process execution failed. 17 | @discardableResult 18 | public func system(command: String, parameters: [String], captureOutput: Bool = false, 19 | currentDirectoryPath: String? = nil) throws -> ProcessResult { 20 | let executablePath = /*command.hasPrefix("/") ? command :*/ try which(program: command) 21 | return try ProcessRunner(command: executablePath, arguments: parameters, 22 | captureOutput: captureOutput, 23 | currentDirectoryPath: currentDirectoryPath).run() 24 | } 25 | 26 | // MARK: Process helpers 27 | 28 | /// Executes command with parameters as a child process 29 | /// 30 | /// `system` will wait for the process to finish, blocking the current thread. 31 | /// 32 | /// - Parameters: 33 | /// - command: Command to execute (full binary path or name of executable in $PATH) and list of parameters, if any. 34 | /// - captureOutput: If output is captured, both stdout and strerr will be available in 35 | /// the return object. Otherwise, process output will be forwarded to stdout and stderr. 36 | /// Defaults to `false`. 37 | /// - currentDirectoryPath: Specify current directory for child process (optional) 38 | /// - Returns: Process result data which is available after process termination. 39 | /// The `ProcessResult` object includes exit code or termination signal and any captured output. 40 | /// - Throws: `SystemError.waitpid` if process execution failed. 41 | @discardableResult 42 | public func system(command: [String], captureOutput: Bool = false, 43 | currentDirectoryPath: String? = nil) throws -> ProcessResult { 44 | guard let executable = command.first else { 45 | throw SystemError.missingCommand 46 | } 47 | return try system(command: executable, parameters: Array(command[1...]), 48 | captureOutput: captureOutput, currentDirectoryPath: currentDirectoryPath) 49 | } 50 | 51 | /// Executes command with parameters as a child process 52 | /// 53 | /// `system` will wait for the process to finish, blocking the current thread. 54 | /// 55 | /// - Parameters: 56 | /// - command: Command to execute (full binary path or name of executable in $PATH) and list of parameters. 57 | /// Parameters will be split by spaces. 58 | /// - captureOutput: If output is captured, both stdout and strerr will be available in 59 | /// the return object. Otherwise, process output will be forwarded to stdout and stderr. 60 | /// Defaults to `false`. 61 | /// - currentDirectoryPath: Specify current directory for child process (optional) 62 | /// - Returns: Process result data which is available after process termination. 63 | /// The `ProcessResult` object includes exit code or termination signal and any captured output. 64 | /// - Throws: `SystemError.waitpid` if process execution failed. 65 | @discardableResult 66 | public func system(command: String, captureOutput: Bool = false, 67 | currentDirectoryPath: String? = nil) throws -> ProcessResult { 68 | return try system(command: command.split(separator: " ").map(String.init), 69 | captureOutput: captureOutput, currentDirectoryPath: currentDirectoryPath) 70 | } 71 | 72 | /// Executes command with parameters as a child process 73 | /// 74 | /// `system` will wait for the process to finish, blocking the current thread. 75 | /// 76 | /// - Parameters: 77 | /// - command: Command to execute (full binary path or name of executable in $PATH) and list of parameters, if any. 78 | /// - captureOutput: If output is captured, both stdout and strerr will be available in 79 | /// the return object. Otherwise, process output will be forwarded to stdout and stderr. 80 | /// Defaults to `false`. 81 | /// - currentDirectoryPath: Specify current directory for child process (optional) 82 | /// - Returns: Process result data which is available after process termination. 83 | /// The `ProcessResult` object includes exit code or termination signal and any captured output. 84 | /// - Throws: `SystemError.waitpid` if process execution failed. 85 | @discardableResult 86 | public func system(command: String..., captureOutput: Bool = false, 87 | currentDirectoryPath: String? = nil) throws -> ProcessResult { 88 | return try system(command: command, captureOutput: captureOutput, currentDirectoryPath: currentDirectoryPath) 89 | } 90 | 91 | // MARK: - Shell helpers 92 | 93 | /// Executes command with parameters in a subshell 94 | /// 95 | /// `system` will wait for the shell process to finish, blocking the current thread. 96 | /// 97 | /// - Parameters: 98 | /// - shell: Shell command with parameters to execute in a subshell with `sh -c`. 99 | /// - captureOutput: If output is captured, both stdout and strerr will be available in 100 | /// the return object. Otherwise, process output will be forwarded to stdout and stderr. 101 | /// Defaults to `false`. 102 | /// - currentDirectoryPath: Specify current directory for child process (optional) 103 | /// - Returns: Process result data which is available after process termination. 104 | /// The `ProcessResult` object includes exit code or termination signal and any captured output. 105 | /// - Throws: `SystemError.waitpid` if process execution failed. 106 | @discardableResult 107 | public func system(shell: String, captureOutput: Bool = false, 108 | currentDirectoryPath: String? = nil) throws -> ProcessResult { 109 | return try system(command: "/bin/sh", parameters: ["-c", shell], 110 | captureOutput: captureOutput, currentDirectoryPath: currentDirectoryPath) 111 | } 112 | 113 | // MARK: - Other helpers 114 | 115 | enum SystemError: Error { 116 | case missingCommand 117 | } 118 | 119 | func which(program: String) throws -> String { 120 | let result = try ProcessRunner(command: "/usr/bin/env", arguments: ["which", program], captureOutput: true).run() 121 | return result.standardOutput 122 | } 123 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import ProcessRunnerTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += ProcessRunnerTests.__allTests() 7 | 8 | XCTMain(tests) 9 | -------------------------------------------------------------------------------- /Tests/ProcessRunnerTests/CurrentDirectoryTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import ProcessRunner 3 | 4 | final class CurrentDirectoryTests: XCTestCase { 5 | 6 | func testCurrentDirectory() throws { 7 | XCTAssertEqual(try system(command: "pwd", captureOutput: true, currentDirectoryPath: "/").standardOutput, "/") 8 | } 9 | 10 | func testCurrentDirectoryCommand() throws { 11 | let content = UUID().uuidString 12 | try content.write(toFile: "/tmp/foo-command", atomically: true, encoding: .utf8) 13 | 14 | XCTAssertEqual(try system(command: "cat /tmp/foo-command", captureOutput: true).standardOutput, content) 15 | XCTAssertEqual(try system(command: "cat foo-command", captureOutput: true, 16 | currentDirectoryPath: "/tmp").standardOutput, content) 17 | } 18 | 19 | func testCurrentDirectoryShell() throws { 20 | let content = UUID().uuidString 21 | try content.write(toFile: "/tmp/foo-shell", atomically: true, encoding: .utf8) 22 | 23 | XCTAssertEqual(try system(shell: "cat /tmp/foo-shell", captureOutput: true).standardOutput, content) 24 | XCTAssertEqual(try system(shell: "cat foo-shell", captureOutput: true, 25 | currentDirectoryPath: "/tmp").standardOutput, content) 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /Tests/ProcessRunnerTests/ProcessRunnerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import ProcessRunner 3 | 4 | final class ProcessRunnerTests: XCTestCase { 5 | 6 | func testWhich() throws { 7 | let result = try ProcessRunner(command: "/usr/bin/env", arguments: ["which", "which"], 8 | captureOutput: true).run() 9 | XCTAssertEqual(result.standardOutput, "/usr/bin/which") 10 | } 11 | 12 | func testShell() throws { 13 | let result = try ProcessRunner(command: "/usr/bin/env", arguments: ["which", "sh"], 14 | captureOutput: true).run() 15 | XCTAssertEqual(result.standardOutput, "/bin/sh") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/ProcessRunnerTests/StandardErrorTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import ProcessRunner 3 | 4 | final class StandardErrorTests: XCTestCase { 5 | 6 | var expectedError: String { 7 | #if os(Linux) 8 | return "cat: unrecognized option" 9 | #else 10 | return "cat: illegal option" 11 | #endif 12 | } 13 | 14 | func testStandardError() throws { 15 | XCTAssertTrue(try system(command: "cat --foobar", captureOutput: true).standardError.contains(expectedError)) 16 | } 17 | 18 | func testStandardErrorShell() throws { 19 | XCTAssertTrue(try system(shell: "cat --foobar", captureOutput: true).standardError.contains(expectedError)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/ProcessRunnerTests/StandardOutputTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import ProcessRunner 3 | 4 | final class StandardOutputTests: XCTestCase { 5 | 6 | func testEchoCombined() throws { 7 | XCTAssertEqual(try system(command: "echo hello world", captureOutput: true).standardOutput, "hello world") 8 | } 9 | 10 | func testEchoSplit() throws { 11 | let output = try system(command: "echo", parameters: ["hello", "world"], captureOutput: true).standardOutput 12 | XCTAssertEqual(output, "hello world") 13 | } 14 | 15 | func testEchoEscaping() throws { 16 | let output = try system(command: "echo", parameters: ["hello world"], captureOutput: true).standardOutput 17 | XCTAssertEqual(output, "hello world") 18 | } 19 | 20 | func testPipe() throws { 21 | let output = try system(command: "echo hello cat | cat", captureOutput: true).standardOutput 22 | XCTAssertEqual(output, "hello cat | cat") 23 | } 24 | 25 | func testRedirect() throws { 26 | let output = try system(command: "echo hello cat > cat && cat cat", captureOutput: true).standardOutput 27 | XCTAssertEqual(output, "hello cat > cat && cat cat") 28 | } 29 | 30 | func testShell() throws { 31 | let path = try system(command: "sh -c pwd", captureOutput: true).standardOutput 32 | let parent = try system(command: "sh", parameters: ["-c", "cd .. && pwd"], captureOutput: true).standardOutput 33 | XCTAssertNotEqual(path, parent) 34 | XCTAssertTrue(path.hasPrefix(parent)) 35 | } 36 | 37 | func testShellPipe() throws { 38 | let output = try system(command: "sh", "-c", "echo foo bar | awk '{print $2}'", 39 | captureOutput: true).standardOutput 40 | XCTAssertEqual(output, "bar") 41 | } 42 | 43 | func testShellRedirect() throws { 44 | let result = try system(shell: "echo hello cat > cat && cat cat && rm cat", captureOutput: true) 45 | XCTAssertEqual(result.standardOutput, "hello cat") 46 | } 47 | 48 | func testShellRedirectPipe() throws { 49 | let output = try system(shell: "echo hello cat > cat && cat cat | awk '{print $2}' && rm cat", 50 | captureOutput: true).standardOutput 51 | XCTAssertEqual(output, "cat") 52 | } 53 | 54 | func testNoStandardOutput() throws { 55 | XCTAssertEqual(try system(command: "cat --foobar", captureOutput: true).standardOutput, "") 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Tests/ProcessRunnerTests/SuccessFailureTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import ProcessRunner 3 | 4 | final class SuccessFailureTests: XCTestCase { 5 | 6 | func testSuccess() throws { 7 | XCTAssertTrue(try system(shell: "echo foo bar").success) 8 | } 9 | 10 | func testFailure() throws { 11 | XCTAssertFalse(try system(shell: "echoooooo foo bar").success) 12 | } 13 | 14 | func testProcessNotFound() { 15 | XCTAssertThrowsError(try system(command: "echoooooo foo bar")) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/ProcessRunnerTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(ObjectiveC) 2 | import XCTest 3 | 4 | extension CurrentDirectoryTests { 5 | // DO NOT MODIFY: This is autogenerated, use: 6 | // `swift test --generate-linuxmain` 7 | // to regenerate. 8 | static let __allTests__CurrentDirectoryTests = [ 9 | ("testCurrentDirectory", testCurrentDirectory), 10 | ("testCurrentDirectoryCommand", testCurrentDirectoryCommand), 11 | ("testCurrentDirectoryShell", testCurrentDirectoryShell), 12 | ] 13 | } 14 | 15 | extension ProcessRunnerTests { 16 | // DO NOT MODIFY: This is autogenerated, use: 17 | // `swift test --generate-linuxmain` 18 | // to regenerate. 19 | static let __allTests__ProcessRunnerTests = [ 20 | ("testShell", testShell), 21 | ("testWhich", testWhich), 22 | ] 23 | } 24 | 25 | extension StandardErrorTests { 26 | // DO NOT MODIFY: This is autogenerated, use: 27 | // `swift test --generate-linuxmain` 28 | // to regenerate. 29 | static let __allTests__StandardErrorTests = [ 30 | ("testStandardError", testStandardError), 31 | ("testStandardErrorShell", testStandardErrorShell), 32 | ] 33 | } 34 | 35 | extension StandardOutputTests { 36 | // DO NOT MODIFY: This is autogenerated, use: 37 | // `swift test --generate-linuxmain` 38 | // to regenerate. 39 | static let __allTests__StandardOutputTests = [ 40 | ("testEchoCombined", testEchoCombined), 41 | ("testEchoEscaping", testEchoEscaping), 42 | ("testEchoSplit", testEchoSplit), 43 | ("testNoStandardOutput", testNoStandardOutput), 44 | ("testPipe", testPipe), 45 | ("testRedirect", testRedirect), 46 | ("testShell", testShell), 47 | ("testShellPipe", testShellPipe), 48 | ("testShellRedirect", testShellRedirect), 49 | ("testShellRedirectPipe", testShellRedirectPipe), 50 | ] 51 | } 52 | 53 | extension SuccessFailureTests { 54 | // DO NOT MODIFY: This is autogenerated, use: 55 | // `swift test --generate-linuxmain` 56 | // to regenerate. 57 | static let __allTests__SuccessFailureTests = [ 58 | ("testFailure", testFailure), 59 | ("testProcessNotFound", testProcessNotFound), 60 | ("testSuccess", testSuccess), 61 | ] 62 | } 63 | 64 | public func __allTests() -> [XCTestCaseEntry] { 65 | return [ 66 | testCase(CurrentDirectoryTests.__allTests__CurrentDirectoryTests), 67 | testCase(ProcessRunnerTests.__allTests__ProcessRunnerTests), 68 | testCase(StandardErrorTests.__allTests__StandardErrorTests), 69 | testCase(StandardOutputTests.__allTests__StandardOutputTests), 70 | testCase(SuccessFailureTests.__allTests__SuccessFailureTests), 71 | ] 72 | } 73 | #endif 74 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: 2 | layout: "reach, diff, flags, files" 3 | behavior: default 4 | require_changes: false # if true: only post the comment if coverage changes 5 | require_base: no # [yes :: must have a base report to post] 6 | require_head: yes # [yes :: must have a head report to post] 7 | branches: null 8 | 9 | -------------------------------------------------------------------------------- /docs/Package.md: -------------------------------------------------------------------------------- 1 | # Package: **System** 2 | 3 | ## Products 4 | 5 | List of products in this package: 6 | 7 | | Product | Type | Targets | 8 | | ------- | ---- | ------- | 9 | | System | Library (automatic) | System | 10 | 11 | _Libraries denoted 'automatic' can be both static or dynamic._ 12 | 13 | ## Modules 14 | 15 | ### Program Modules 16 | 17 | | Module | Type | Dependencies | 18 | | ------ | ---- | ------------ | 19 | | System | Regular | | 20 | 21 | ### Test Modules 22 | 23 | | Module | Type | Dependencies | 24 | | ------ | ---- | ------------ | 25 | | SystemTests | Test | System | 26 | 27 | ### Module Dependency Graph 28 | 29 | [![Module Dependency Graph](PackageModules.png)](PackageModules.png) 30 | 31 | ## External Dependencies 32 | 33 | This package has zero dependencies 🎉 34 | 35 | ## Requirements 36 | 37 | ### Minimum Required Versions 38 | 39 | | Platform | Version | 40 | | -------- | ------- | 41 | | macOS | 10.13 | 42 | 43 | This file was generated by [SourceDocs](https://github.com/eneko/SourceDocs) on 2020-05-18 15:00:25 +0000 -------------------------------------------------------------------------------- /docs/PackageModules.dot: -------------------------------------------------------------------------------- 1 | digraph ModuleDependencyGraph { 2 | rankdir = LR 3 | graph [fontname="Helvetica-light", style = filled, color = "#eaeaea"] 4 | node [shape=box, fontname="Helvetica", style=filled] 5 | edge [color="#545454"] 6 | 7 | subgraph clusterRegular { 8 | label = "Program Modules" 9 | node [color="#caecec"] 10 | "System" 11 | } 12 | subgraph clusterTest { 13 | label = "Test Modules" 14 | node [color="#aaccee"] 15 | "SystemTests" 16 | } 17 | 18 | 19 | "SystemTests" -> "System" 20 | } -------------------------------------------------------------------------------- /docs/PackageModules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eneko/ProcessRunner/bedc55797b206bff50f652ffad8269223320fee5/docs/PackageModules.png -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Reference Documentation 2 | 3 | ## Structs 4 | 5 | - [ProcessResult](structs/ProcessResult.md) 6 | 7 | ## Classes 8 | 9 | - [ProcessRunner](classes/ProcessRunner.md) 10 | 11 | ## Methods 12 | 13 | - [system(command_captureOutput_currentDirectoryPath_)](methods/system(command_captureOutput_currentDirectoryPath_).md) 14 | - [system(command_captureOutput_currentDirectoryPath_)](methods/system(command_captureOutput_currentDirectoryPath_).md) 15 | - [system(command_captureOutput_currentDirectoryPath_)](methods/system(command_captureOutput_currentDirectoryPath_).md) 16 | - [system(command_parameters_captureOutput_currentDirectoryPath_)](methods/system(command_parameters_captureOutput_currentDirectoryPath_).md) 17 | - [system(shell_captureOutput_currentDirectoryPath_)](methods/system(shell_captureOutput_currentDirectoryPath_).md) 18 | 19 | This reference documentation was generated with 20 | [SourceDocs](https://github.com/eneko/SourceDocs). 21 | 22 | Generated at 2020-05-18 15:00:24 +0000 -------------------------------------------------------------------------------- /docs/classes/ProcessRunner.md: -------------------------------------------------------------------------------- 1 | **CLASS** 2 | 3 | # `ProcessRunner` 4 | 5 | ```swift 6 | public class ProcessRunner 7 | ``` 8 | 9 | > Process runner to execute system commands 10 | 11 | ## Properties 12 | ### `command` 13 | 14 | ```swift 15 | public let command: String 16 | ``` 17 | 18 | ### `arguments` 19 | 20 | ```swift 21 | public let arguments: [String] 22 | ``` 23 | 24 | ### `captureOutput` 25 | 26 | ```swift 27 | public let captureOutput: Bool 28 | ``` 29 | 30 | ### `currentDirectoryPath` 31 | 32 | ```swift 33 | public let currentDirectoryPath: String? 34 | ``` 35 | 36 | ## Methods 37 | ### `init(command:arguments:captureOutput:currentDirectoryPath:)` 38 | 39 | ```swift 40 | public init(command: String, arguments: [String], captureOutput: Bool, currentDirectoryPath: String? = nil) 41 | ``` 42 | 43 | > Initialize process runner 44 | > - Parameters: 45 | > - command: Absolute path to the binary executable 46 | > - arguments: List of arguments to pass to the executable 47 | > - captureOutput: Capture stdout and stderr 48 | > - currentDirectoryPath: Specify current directory for child process (optional) 49 | 50 | #### Parameters 51 | 52 | | Name | Description | 53 | | ---- | ----------- | 54 | | command | Absolute path to the binary executable | 55 | | arguments | List of arguments to pass to the executable | 56 | | captureOutput | Capture stdout and stderr | 57 | | currentDirectoryPath | Specify current directory for child process (optional) | 58 | 59 | ### `run()` 60 | 61 | ```swift 62 | public func run() throws -> ProcessResult 63 | ``` 64 | 65 | > Execute the process 66 | > - Throws: Throws error if process fails to run 67 | > - Returns: Process execution result, including status code and captured output 68 | -------------------------------------------------------------------------------- /docs/methods/system(command_captureOutput_currentDirectoryPath_).md: -------------------------------------------------------------------------------- 1 | ### `system(command:captureOutput:currentDirectoryPath:)` 2 | 3 | ```swift 4 | public func system(command: String..., captureOutput: Bool = false, 5 | currentDirectoryPath: String? = nil) throws -> ProcessResult 6 | ``` 7 | 8 | > Executes command with parameters as a child process 9 | > 10 | > `system` will wait for the process to finish, blocking the current thread. 11 | > 12 | > - Parameters: 13 | > - command: Command to execute (full binary path or name of executable in $PATH) and list of parameters, if any. 14 | > - captureOutput: If output is captured, both stdout and strerr will be available in 15 | > the return object. Otherwise, process output will be forwarded to stdout and stderr. 16 | > Defaults to `false`. 17 | > - currentDirectoryPath: Specify current directory for child process (optional) 18 | > - Returns: Process result data which is available after process termination. 19 | > The `ProcessResult` object includes exit code or termination signal and any captured output. 20 | > - Throws: `SystemError.waitpid` if process execution failed. 21 | 22 | #### Parameters 23 | 24 | | Name | Description | 25 | | ---- | ----------- | 26 | | command | Command to execute (full binary path or name of executable in $PATH) and list of parameters, if any. | 27 | | captureOutput | If output is captured, both stdout and strerr will be available in the return object. Otherwise, process output will be forwarded to stdout and stderr. Defaults to `false`. | 28 | | currentDirectoryPath | Specify current directory for child process (optional) | -------------------------------------------------------------------------------- /docs/methods/system(command_parameters_captureOutput_currentDirectoryPath_).md: -------------------------------------------------------------------------------- 1 | ### `system(command:parameters:captureOutput:currentDirectoryPath:)` 2 | 3 | ```swift 4 | public func system(command: String, parameters: [String], captureOutput: Bool = false, 5 | currentDirectoryPath: String? = nil) throws -> ProcessResult 6 | ``` 7 | 8 | > Executes command with parameters as a child process 9 | > 10 | > `system` will wait for the process to finish, blocking the current thread. 11 | > 12 | > - Parameters: 13 | > - command: Command to execute (full binary path or name of executable in $PATH). 14 | > - parameters: List of parameters to pass to the command. 15 | > - captureOutput: If output is captured, both stdout and strerr will be available in 16 | > the return object. Otherwise, process output will be forwarded to stdout and stderr. 17 | > Defaults to `false`. 18 | > - currentDirectoryPath: Specify current directory for child process (optional) 19 | > - Returns: Process result data which is available after process termination. 20 | > The `ProcessResult` object includes exit code or termination signal and any captured output. 21 | > - Throws: `SystemError.waitpid` if process execution failed. 22 | 23 | #### Parameters 24 | 25 | | Name | Description | 26 | | ---- | ----------- | 27 | | command | Command to execute (full binary path or name of executable in $PATH). | 28 | | parameters | List of parameters to pass to the command. | 29 | | captureOutput | If output is captured, both stdout and strerr will be available in the return object. Otherwise, process output will be forwarded to stdout and stderr. Defaults to `false`. | 30 | | currentDirectoryPath | Specify current directory for child process (optional) | -------------------------------------------------------------------------------- /docs/methods/system(shell_captureOutput_currentDirectoryPath_).md: -------------------------------------------------------------------------------- 1 | ### `system(shell:captureOutput:currentDirectoryPath:)` 2 | 3 | ```swift 4 | public func system(shell: String, captureOutput: Bool = false, 5 | currentDirectoryPath: String? = nil) throws -> ProcessResult 6 | ``` 7 | 8 | > Executes command with parameters in a subshell 9 | > 10 | > `system` will wait for the shell process to finish, blocking the current thread. 11 | > 12 | > - Parameters: 13 | > - shell: Shell command with parameters to execute in a subshell with `sh -c`. 14 | > - captureOutput: If output is captured, both stdout and strerr will be available in 15 | > the return object. Otherwise, process output will be forwarded to stdout and stderr. 16 | > Defaults to `false`. 17 | > - currentDirectoryPath: Specify current directory for child process (optional) 18 | > - Returns: Process result data which is available after process termination. 19 | > The `ProcessResult` object includes exit code or termination signal and any captured output. 20 | > - Throws: `SystemError.waitpid` if process execution failed. 21 | 22 | #### Parameters 23 | 24 | | Name | Description | 25 | | ---- | ----------- | 26 | | shell | Shell command with parameters to execute in a subshell with `sh -c`. | 27 | | captureOutput | If output is captured, both stdout and strerr will be available in the return object. Otherwise, process output will be forwarded to stdout and stderr. Defaults to `false`. | 28 | | currentDirectoryPath | Specify current directory for child process (optional) | -------------------------------------------------------------------------------- /docs/structs/ProcessResult.md: -------------------------------------------------------------------------------- 1 | **STRUCT** 2 | 3 | # `ProcessResult` 4 | 5 | ```swift 6 | public struct ProcessResult 7 | ``` 8 | 9 | > Process result, including captured output if required 10 | 11 | ## Properties 12 | ### `exitStatus` 13 | 14 | ```swift 15 | public let exitStatus: Int 16 | ``` 17 | 18 | > Process exit status code 19 | 20 | ### `standardOutput` 21 | 22 | ```swift 23 | public let standardOutput: String 24 | ``` 25 | 26 | > Standard output, if captured 27 | 28 | ### `standardError` 29 | 30 | ```swift 31 | public let standardError: String 32 | ``` 33 | 34 | > Standard error output, if captured 35 | 36 | ### `success` 37 | 38 | ```swift 39 | public var success: Bool 40 | ``` 41 | 42 | > Returns `true` if the process finalized with exit code 0. 43 | -------------------------------------------------------------------------------- /processrunner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eneko/ProcessRunner/bedc55797b206bff50f652ffad8269223320fee5/processrunner.png --------------------------------------------------------------------------------