├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── .spi.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── ProcessEnv │ ├── Process+Output.swift │ ├── Process+Parameters.swift │ └── ProcessInfo+UserEnvironment.swift └── Tests └── ProcessEnvTests └── ProcessEnvTests.swift /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [mattmassicotte] 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - 'README.md' 9 | - 'CODE_OF_CONDUCT.md' 10 | - '.editorconfig' 11 | - '.spi.yml' 12 | pull_request: 13 | branches: 14 | - main 15 | 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | test: 22 | name: Test 23 | runs-on: macOS-15 24 | timeout-minutes: 30 25 | env: 26 | DEVELOPER_DIR: /Applications/Xcode_16.3.app 27 | steps: 28 | - uses: actions/checkout@v4 29 | - name: Test 30 | run: set -o pipefail && xcodebuild -scheme ProcessEnv -destination "platform=macOS" test | xcbeautify 31 | 32 | linux_test: 33 | name: Test Linux 34 | runs-on: ubuntu-latest 35 | timeout-minutes: 30 36 | strategy: 37 | matrix: 38 | swift-version: 39 | - 6.0.3 40 | - 6.1 41 | steps: 42 | - name: Checkout 43 | uses: actions/checkout@v4 44 | - name: Swiftly 45 | uses: vapor/swiftly-action@v0.2.0 46 | with: 47 | toolchain: ${{ matrix.swift-version }} 48 | - name: Test 49 | run: swift test 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Carthage 4 | /Packages 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [ProcessEnv] 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | support@chimehq.com. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Chime 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.8 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "ProcessEnv", 7 | platforms: [.macOS(.v10_13)], 8 | products: [ 9 | .library(name: "ProcessEnv", targets: ["ProcessEnv"]), 10 | ], 11 | dependencies: [], 12 | targets: [ 13 | .target(name: "ProcessEnv", dependencies: []), 14 | .testTarget(name: "ProcessEnvTests", dependencies: ["ProcessEnv"]), 15 | ] 16 | ) 17 | 18 | let swiftSettings: [SwiftSetting] = [ 19 | .enableExperimentalFeature("StrictConcurrency") 20 | ] 21 | 22 | for target in package.targets { 23 | var settings = target.swiftSettings ?? [] 24 | settings.append(contentsOf: swiftSettings) 25 | target.swiftSettings = settings 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | [![Build Status][build status badge]][build status] 4 | [![Platforms][platforms badge]][platforms] 5 | [![Documentation][documentation badge]][documentation] 6 | [![Matrix][matrix badge]][matrix] 7 | 8 |
9 | 10 | # ProcessEnv 11 | 12 | ProcessEnv is a small library for capturing a user's shell configuration. This is very handy for launching `Process` instances with the same configuration. It also contains a few niceties for working with `Process`. 13 | 14 | ## Integration 15 | 16 | Swift Package Manager: 17 | 18 | ```swift 19 | dependencies: [ 20 | .package(url: "https://github.com/ChimeHQ/ProcessEnv", branch: "main") 21 | ] 22 | ``` 23 | 24 | ## Extensions 25 | 26 | The bulk of code is in the form of extensions on `ProcessInfo`, for accessing various environment variables. A single `[String : String]` dictionary of the user's environment is available as the `userEnvironment` property. 27 | 28 | ```swift 29 | ProcessInfo.processInfo.userEnvironment 30 | 31 | ProcessInfo.processInfo.path // $PATH 32 | ProcessInfo.processInfo.homePath 33 | ProcessInfo.processInfo.shellExecutablePath 34 | ``` 35 | 36 | ## Contributing and Collaboration 37 | 38 | I would love to hear from you! Issues or pull requests work great. Both a [Matrix space][matrix] and [Discord][discord] are available for live help, but I have a strong bias towards answering in the form of documentation. You can also find me on [the web](https://www.massicotte.org). 39 | 40 | I prefer collaboration, and would love to find ways to work together if you have a similar project. 41 | 42 | I prefer indentation with tabs for improved accessibility. But, I'd rather you use the system you want and make a PR than hesitate because of whitespace. 43 | 44 | By participating in this project you agree to abide by the [Contributor Code of Conduct](CODE_OF_CONDUCT.md). 45 | 46 | [build status]: https://github.com/ChimeHQ/ProcessEnv/actions 47 | [build status badge]: https://github.com/ChimeHQ/ProcessEnv/workflows/CI/badge.svg 48 | [platforms]: https://swiftpackageindex.com/ChimeHQ/ProcessEnv 49 | [platforms badge]: https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FChimeHQ%2FProcessEnv%2Fbadge%3Ftype%3Dplatforms 50 | [documentation]: https://swiftpackageindex.com/ChimeHQ/ProcessEnv/main/documentation 51 | [documentation badge]: https://img.shields.io/badge/Documentation-DocC-blue 52 | [matrix]: https://matrix.to/#/%23chimehq%3Amatrix.org 53 | [matrix badge]: https://img.shields.io/matrix/chimehq%3Amatrix.org?label=Matrix 54 | [discord]: https://discord.gg/esFpX6sErJ 55 | -------------------------------------------------------------------------------- /Sources/ProcessEnv/Process+Output.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension Process { 4 | static func execute(_ parameters: Process.ExecutionParameters) throws -> Data? { 5 | let task = Process(parameters: parameters) 6 | 7 | return try? task.runAndReadStdout() 8 | } 9 | 10 | static func executeAsUser(_ parameters: Process.ExecutionParameters) throws -> Data? { 11 | let userParams = parameters.userShellInvocation() 12 | 13 | let task = Process(parameters: userParams) 14 | 15 | return try? task.runAndReadStdout() 16 | } 17 | 18 | static func readOutput(from launchPath: String, arguments: [String] = [], environment: [String : String] = [:]) -> Data? { 19 | let params = Process.ExecutionParameters(path: launchPath, arguments: arguments, environment: environment) 20 | 21 | return try? execute(params) 22 | } 23 | 24 | func runAndReadStdout() throws -> Data? { 25 | let pipe = Pipe() 26 | 27 | standardOutput = pipe 28 | 29 | if #available(macOS 10.13, *) { 30 | try run() 31 | } else { 32 | if let launchPath = launchPath, FileManager.default.isExecutableFile(atPath: launchPath) == false { 33 | return nil 34 | } 35 | 36 | launch() 37 | } 38 | 39 | waitUntilExit() 40 | 41 | if #available(macOS 10.15.4, *) { 42 | return try pipe.fileHandleForReading.readToEnd() 43 | } else { 44 | return pipe.fileHandleForReading.readDataToEndOfFile() 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/ProcessEnv/Process+Parameters.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if os(macOS) || os(Linux) 4 | 5 | public extension Process { 6 | /// Wraps up all of the parameters needed for starting a Process into one single type. 7 | struct ExecutionParameters: Codable, Hashable, Sendable { 8 | public var path: String 9 | public var arguments: [String] 10 | public var environment: [String : String]? 11 | public var currentDirectoryURL: URL? 12 | 13 | public init(path: String, arguments: [String] = [], environment: [String : String]? = nil, currentDirectoryURL: URL? = nil) { 14 | self.path = path 15 | self.arguments = arguments 16 | self.environment = environment 17 | self.currentDirectoryURL = currentDirectoryURL 18 | } 19 | 20 | public var command: String { 21 | let escapedArgs = arguments.map { arg in 22 | let range = arg.rangeOfCharacter(from: .whitespacesAndNewlines) 23 | 24 | if let range, range.isEmpty == false { 25 | return "\"" + arg + "\"" 26 | } 27 | 28 | return arg 29 | } 30 | 31 | return ([path] + escapedArgs).joined(separator: " ") 32 | } 33 | 34 | /// Returns parameters that emulate an invocation in the user's shell 35 | /// 36 | /// This is done by executing: 37 | /// 38 | /// shellExecutablePath -ilc 39 | /// 40 | /// This method executes this with the `environment` environment 41 | /// variables set. But, it also ensures that the `TERM`, `HOME`, and 42 | /// `PATH` variables have values, if aren't present in `environment`. 43 | /// 44 | /// The `-i` and `-l` flags are critical, as they control how many 45 | /// shells read configuration files. 46 | public func userShellInvocation() -> ExecutionParameters { 47 | let processInfo = ProcessInfo.processInfo 48 | 49 | let shellPath = processInfo.shellExecutablePath 50 | let args = ["-ilc", command] 51 | let cwdURL = currentDirectoryURL 52 | 53 | let defaultEnv = ["TERM": "xterm-256color", 54 | "HOME": processInfo.homePath, 55 | "PATH": processInfo.path] 56 | 57 | let baseEnv = environment ?? defaultEnv 58 | 59 | let env = baseEnv.merging(defaultEnv, uniquingKeysWith: { (a, _) in a }) 60 | 61 | return ExecutionParameters(path: shellPath, 62 | arguments: args, 63 | environment: env, 64 | currentDirectoryURL: cwdURL) 65 | } 66 | } 67 | 68 | private var compatibleCurrentDirectoryURL: URL? { 69 | get { 70 | if #available(macOS 10.13, *) { 71 | return currentDirectoryURL 72 | } else { 73 | return URL(fileURLWithPath: currentDirectoryPath, isDirectory: true) 74 | } 75 | } 76 | set { 77 | if #available(macOS 10.13, *) { 78 | currentDirectoryURL = newValue 79 | return 80 | } 81 | 82 | if let cwdPath = newValue?.path { 83 | self.currentDirectoryPath = cwdPath 84 | } 85 | } 86 | } 87 | 88 | var parameters: ExecutionParameters { 89 | get { 90 | return ExecutionParameters(path: self.launchPath ?? "", 91 | arguments: arguments ?? [], 92 | environment: self.environment, 93 | currentDirectoryURL: self.compatibleCurrentDirectoryURL) 94 | } 95 | set { 96 | self.launchPath = newValue.path 97 | self.arguments = newValue.arguments 98 | self.environment = newValue.environment 99 | self.compatibleCurrentDirectoryURL = newValue.currentDirectoryURL 100 | 101 | } 102 | } 103 | 104 | convenience init(parameters: ExecutionParameters) { 105 | self.init() 106 | 107 | self.parameters = parameters 108 | } 109 | } 110 | 111 | #endif 112 | -------------------------------------------------------------------------------- /Sources/ProcessEnv/ProcessInfo+UserEnvironment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProcessInfo+UserEnvironment.swift 3 | // ProcessEnv 4 | // 5 | // Created by Matthew Massicotte on 2019-02-12. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Process { 11 | @available(*, deprecated, renamed: "Environment") 12 | typealias Envrionment = [String : String] 13 | typealias Environment = [String : String] 14 | } 15 | 16 | extension ProcessInfo { 17 | /// The path to the current user's shell executable 18 | /// 19 | /// This attempts to query the `SHELL` environment variable, the 20 | /// password directory (via `getpwuid`), or if those fail 21 | /// falls back to "/bin/bash". 22 | public var shellExecutablePath: String { 23 | if let value = environment["SHELL"], !value.isEmpty { 24 | return value 25 | } 26 | 27 | if let value = pwShell, !value.isEmpty { 28 | return value 29 | } 30 | 31 | // this is a terrible fallback, but we need something 32 | return "/bin/bash" 33 | } 34 | 35 | public var pwShell: String? { 36 | guard let passwd = getpwuid(getuid()) else { 37 | return nil 38 | } 39 | 40 | guard let cString = passwd.pointee.pw_shell else { 41 | return nil 42 | } 43 | 44 | return String(cString: cString) 45 | } 46 | 47 | public var pwUserName: String? { 48 | guard let passwd = getpwuid(getuid()) else { 49 | return nil 50 | } 51 | 52 | guard let cString = passwd.pointee.pw_name else { 53 | return nil 54 | } 55 | 56 | return String(cString: cString) 57 | } 58 | 59 | public var pwDir: String? { 60 | guard let passwd = getpwuid(getuid()) else { 61 | return nil 62 | } 63 | 64 | guard let cString = passwd.pointee.pw_dir else { 65 | return nil 66 | } 67 | 68 | return String(cString: cString) 69 | } 70 | /// Returns the value of PATH 71 | /// 72 | /// If PATH is set in the environment, it is returned. If not, 73 | /// the fallback value of "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" 74 | /// is returned. 75 | public var path: String { 76 | return environment["PATH"] ?? "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" 77 | } 78 | 79 | public var homePath: String { 80 | if let path = environment["HOME"] { 81 | return path 82 | } 83 | 84 | if let path = pwDir { 85 | return path 86 | } 87 | 88 | if #available(macOS 12.0, *) { 89 | return "/Users/\(userName)" 90 | } 91 | 92 | if let name = pwUserName { 93 | return "/Users/\(name)" 94 | } 95 | 96 | // I'm not sure there is a reasonable fallback in this situation 97 | return "" 98 | } 99 | 100 | public var sandboxContainerId: String? { 101 | environment["APP_SANDBOX_CONTAINER_ID"] 102 | } 103 | 104 | /// Returns true if the process is running with sandboxing enabled. 105 | public var isSandboxed: Bool { 106 | sandboxContainerId != nil 107 | } 108 | 109 | /// Capture the interactive-login shell environment 110 | /// 111 | /// This method attempts to reconstruct the user 112 | /// environment that would be set up when logging into 113 | /// a terminal session. 114 | public var userEnvironment: [String : String] { 115 | guard let data = try? Process.executeAsUser(Process.ExecutionParameters(path: "/usr/bin/env", environment: environment)) else { 116 | return environment 117 | } 118 | 119 | return parseEnvOutput(data) 120 | } 121 | 122 | func parseEnvOutput(_ data: Data) -> [String : String] { 123 | guard let string = String(data: data, encoding: .utf8) else { 124 | return [:] 125 | } 126 | 127 | var env: [String: String] = [:] 128 | let charSet = CharacterSet.whitespaces 129 | 130 | string.enumerateLines { (line, _) in 131 | let components = line.split(separator: "=") 132 | 133 | guard let key = components.first?.trimmingCharacters(in: charSet) else { 134 | return 135 | } 136 | 137 | let value = components.dropFirst().joined(separator: "=").trimmingCharacters(in: charSet) 138 | 139 | env[key] = value 140 | } 141 | 142 | return env 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Tests/ProcessEnvTests/ProcessEnvTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ProcessEnv 3 | 4 | final class ProcessEnvTests: XCTestCase { 5 | func testParseEnvOutput() throws { 6 | let output = """ 7 | TERM=xterm-256color 8 | TERM_PROGRAM=Apple_Terminal 9 | """ 10 | 11 | let data = ProcessInfo.processInfo.parseEnvOutput(output.data(using: .utf8)!) 12 | 13 | XCTAssertEqual(data, ["TERM" : "xterm-256color", "TERM_PROGRAM" : "Apple_Terminal"]) 14 | } 15 | 16 | func testEnvOutputWithEqualsInValue() throws { 17 | let output = """ 18 | KEY=tricky=value 19 | """ 20 | 21 | let data = ProcessInfo.processInfo.parseEnvOutput(output.data(using: .utf8)!) 22 | 23 | XCTAssertEqual(data, ["KEY" : "tricky=value"]) 24 | } 25 | 26 | #if !os(Linux) 27 | func testEnvironmentVariables() throws { 28 | let env = ProcessInfo.processInfo.userEnvironment 29 | 30 | XCTAssertFalse(env.isEmpty) 31 | 32 | XCTAssertNotNil(env["SHELL"]) 33 | XCTAssertNotNil(env["HOME"]) 34 | } 35 | #endif 36 | 37 | func testParameterCommand() throws { 38 | let params = Process.ExecutionParameters(path: "cmd", arguments: ["-u", "-v"]) 39 | 40 | XCTAssertEqual(params.command, "cmd -u -v") 41 | } 42 | 43 | func testParameterCommandWithSpaces() throws { 44 | let params = Process.ExecutionParameters(path: "cmd", arguments: ["-u", "has spaces"]) 45 | 46 | XCTAssertEqual(params.command, "cmd -u \"has spaces\"") } 47 | 48 | func testUserShellParameters() throws { 49 | let params = Process.ExecutionParameters(path: "cmd", arguments: ["-u", "-v"]) 50 | 51 | let userParams = params.userShellInvocation() 52 | 53 | XCTAssertEqual(userParams.path, ProcessInfo.processInfo.shellExecutablePath) 54 | XCTAssertEqual(userParams.arguments, ["-ilc", "cmd -u -v"]) 55 | } 56 | 57 | func testUserShellParametersWithSpaces() throws { 58 | let params = Process.ExecutionParameters(path: "cmd", arguments: ["-u", "white\nspace"]) 59 | 60 | let userParams = params.userShellInvocation() 61 | 62 | XCTAssertEqual(userParams.path, ProcessInfo.processInfo.shellExecutablePath) 63 | XCTAssertEqual(userParams.arguments, ["-ilc", "cmd -u \"white\nspace\""]) 64 | } 65 | } 66 | --------------------------------------------------------------------------------